From e00cd12bb925ab7a0294ee8b0a4289788c7184f1 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Thu, 21 May 2026 11:24:18 -0400 Subject: [PATCH] some performance improvements --- lib/controllers/client_controller.dart | 7 +- lib/controllers/event_controller.dart | 2 +- .../members_by_status_controller.dart | 8 +-- lib/controllers/members_controller.dart | 20 +++--- lib/controllers/power_level_controller.dart | 7 +- lib/controllers/room_chat_controller.dart | 60 ++++++++-------- lib/controllers/rooms_controller.dart | 71 +++++++++---------- lib/controllers/via_controller.dart | 14 ++-- lib/models/room.dart | 45 ++++++++---- lib/widgets/room_chat.dart | 13 ++-- 10 files changed, 130 insertions(+), 117 deletions(-) diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index 3f29f25..5ccdc27 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,7 +1,7 @@ import "dart:ffi"; import "dart:io"; import "dart:isolate"; -import "package:collection/collection.dart"; +import "dart:math"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:ffi/ffi.dart"; import "package:flutter/foundation.dart"; @@ -237,9 +237,8 @@ class ClientController extends AsyncNotifier { _sendCommand("set_membership", request.toJson()); Future markRead(Room room) async { - final event = room.events.firstWhereOrNull( - (event) => event.rowId == room.timeline.last.eventRowId, - ); + final eventRowId = room.timeline[room.timeline.keys.reduce(max)]; + final event = eventRowId == null ? null : room.events[eventRowId]; if (event == null || room.metadata == null) return; await _sendCommand("mark_read", { diff --git a/lib/controllers/event_controller.dart b/lib/controllers/event_controller.dart index e195f4d..94992ca 100644 --- a/lib/controllers/event_controller.dart +++ b/lib/controllers/event_controller.dart @@ -14,7 +14,7 @@ class EventController extends AsyncNotifier { final room = ref.watch( RoomsController.provider.select((value) => value[request.roomId]), ); - final event = room?.events.firstWhereOrNull( + final event = room?.events.values.firstWhereOrNull( (event) => event.eventId == request.eventId, ); diff --git a/lib/controllers/members_by_status_controller.dart b/lib/controllers/members_by_status_controller.dart index 44b9c54..2b49903 100644 --- a/lib/controllers/members_by_status_controller.dart +++ b/lib/controllers/members_by_status_controller.dart @@ -5,12 +5,12 @@ import "package:nexus/models/configs/members_by_status_config.dart"; import "package:nexus/models/content/membership.dart"; import "package:nexus/models/event.dart"; -class MembersByStatusController extends AsyncNotifier> { +class MembersByStatusController extends AsyncNotifier> { final MembersByStatusConfig config; MembersByStatusController(this.config); @override - Future> build() => ref.watch( + Future> build() => ref.watch( MembersController.provider(config.roomId).selectAsync( (members) => members .where( @@ -19,14 +19,14 @@ class MembersByStatusController extends AsyncNotifier> { _ => false, }, ) - .toIList(), + .toISet(), ), ); static final provider = AsyncNotifierProvider.family< MembersByStatusController, - IList, + ISet, MembersByStatusConfig >(MembersByStatusController.new); } diff --git a/lib/controllers/members_controller.dart b/lib/controllers/members_controller.dart index 757968f..64e1ef7 100644 --- a/lib/controllers/members_controller.dart +++ b/lib/controllers/members_controller.dart @@ -1,4 +1,3 @@ -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"; @@ -7,17 +6,17 @@ import "package:nexus/models/content/content.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/requests/get_room_state_request.dart"; -class MembersController extends AsyncNotifier> { +class MembersController extends AsyncNotifier> { final String roomId; MembersController(this.roomId); @override - Future> build() async { + Future> build() async { final room = ref.watch( RoomsController.provider.select((value) => value[roomId]), ); - if (room == null) return const IList.empty(); + if (room == null) return const ISet.empty(); if (!room.hasFetchedMembers) { final fetchedState = await ref @@ -30,21 +29,18 @@ class MembersController extends AsyncNotifier> { ), ); - ref + await ref .read(RoomsController.provider.notifier) .addState(roomId, fetchedState, isMembers: true); } return room.state[EventType.membership.type]?.values - .map( - (rowId) => - room.events.firstWhereOrNull((event) => event.rowId == rowId), - ) + .map((rowId) => room.events[rowId]) .nonNulls - .toIList() ?? - const IList.empty(); + .toISet() ?? + const ISet.empty(); } static final provider = AsyncNotifierProvider.autoDispose - .family, String>(MembersController.new); + .family, String>(MembersController.new); } diff --git a/lib/controllers/power_level_controller.dart b/lib/controllers/power_level_controller.dart index 9ebfcc2..2539778 100644 --- a/lib/controllers/power_level_controller.dart +++ b/lib/controllers/power_level_controller.dart @@ -1,4 +1,3 @@ -import "package:collection/collection.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; @@ -24,9 +23,9 @@ class PowerLevelController extends Notifier { RoomsController.provider.select((value) => value[config.roomId]), ); - final event = room?.events.firstWhereOrNull( - (event) => event.rowId == room.state[EventType.powerLevels.type]?[""], - ); + final eventRowId = room?.state[EventType.powerLevels.type]?[""]; + + final event = eventRowId == null ? null : room?.events[eventRowId]; final content = event?.content is PowerLevelsContent ? event!.content : PowerLevelsContent(); diff --git a/lib/controllers/room_chat_controller.dart b/lib/controllers/room_chat_controller.dart index 64fbfd7..4412b2d 100644 --- a/lib/controllers/room_chat_controller.dart +++ b/lib/controllers/room_chat_controller.dart @@ -1,4 +1,5 @@ import "dart:async"; +import "dart:math"; import "package:collection/collection.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; @@ -33,7 +34,7 @@ class RoomChatController extends AsyncNotifier> { GetRoomStateRequest(roomId: roomId), ); - ref.read(RoomsController.provider.notifier).addState(roomId, state); + await 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. @@ -41,21 +42,21 @@ class RoomChatController extends AsyncNotifier> { loadOlder(); } - return room.timeline.reversed - .map((timeline) { - final foundEvent = room.events.firstWhereOrNull( - (event) => event.rowId == timeline.eventRowId, - ); + return room.timeline + .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0)) + .map((entry) { + if (entry.value == null) return null; - final editedEvent = foundEvent?.lastEditRowId == 0 + final foundEvent = room.events[entry.value!]; + + final editedEvent = + foundEvent == null || foundEvent.lastEditRowId == 0 ? null - : room.events.firstWhereOrNull( - (event) => event.rowId == foundEvent?.lastEditRowId, - ); + : room.events[foundEvent.lastEditRowId]; - return foundEvent?.copyWith( - content: editedEvent?.content ?? foundEvent.content, - ); + return editedEvent == null + ? foundEvent + : foundEvent?.copyWith(content: editedEvent.content); }) .nonNulls .toIList(); @@ -72,34 +73,37 @@ class RoomChatController extends AsyncNotifier> { ); Future loadOlder() async { + final timelineKeys = ref + .read(RoomsController.provider.select((value) => value[roomId])) + ?.timeline + .keys; final response = await ref .watch(ClientController.provider.notifier) .paginate( PaginateRequest( roomId: roomId, - maxTimelineId: ref - .read(RoomsController.provider.select((value) => value[roomId])) - ?.timeline - .firstOrNull - ?.timelineRowId, + maxTimelineId: timelineKeys?.isNotEmpty == true + ? timelineKeys?.reduce(min) + : null, ), ); - ref + await ref .watch(RoomsController.provider.notifier) .update( IMap({ roomId: Room( - events: response.events.addAll(response.relatedEvents), + events: IMap.fromIterable( + response.events.addAll(response.relatedEvents), + keyMapper: (event) => event.rowId, + valueMapper: (event) => event, + ), hasMore: response.hasMore, - timeline: response.events - .map( - (event) => TimelineRowTuple( - timelineRowId: event.timelineRowId, - eventRowId: event.rowId, - ), - ) - .toIList(), + timeline: IMap.fromIterable( + response.events, + keyMapper: (event) => event.timelineRowId, + valueMapper: (event) => event.rowId, + ), ), }), const ISet.empty(), diff --git a/lib/controllers/rooms_controller.dart b/lib/controllers/rooms_controller.dart index 40f5627..55a0d2b 100644 --- a/lib/controllers/rooms_controller.dart +++ b/lib/controllers/rooms_controller.dart @@ -1,4 +1,3 @@ -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"; @@ -9,47 +8,49 @@ 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, - }), - ), - ), + Future addState( + String roomId, + IList state, { + bool isMembers = false, + }) => update( + { + roomId: Room( + events: IMap.fromEntries( + state.map((event) => MapEntry(event.rowId, event)), + ), + 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(), - ); + ), + ), + }.toIMap(), + const ISet.empty(), + ); - void update(IMap rooms, ISet leftRooms) { + Future update(IMap rooms, ISet leftRooms) async { final merged = rooms.entries.fold(state, (acc, entry) { final roomId = entry.key; final incoming = entry.value; final existing = acc[roomId]; - final events = existing?.events.updateById( - incoming.events, - (item) => item.eventId, - ); - return acc.add( roomId, existing?.copyWith( hasMore: incoming.hasMore, metadata: incoming.metadata ?? existing.metadata, - events: events!, + events: incoming.events.isEmpty + ? existing.events + : existing.events.addAll(incoming.events), state: incoming.state.entries.fold( existing.state, (previousValue, event) => previousValue.add( @@ -64,15 +65,9 @@ class RoomsController extends Notifier> { incoming.hasFetchedMembers || existing.hasFetchedMembers, hasFetchedState: incoming.hasFetchedState || existing.hasFetchedState, - timeline: - (incoming.reset - ? incoming.timeline - : existing.timeline.updateById( - incoming.timeline, - (item) => item.timelineRowId, - )) - .sortedBy((element) => element.timelineRowId) - .toIList(), + timeline: (incoming.reset + ? incoming.timeline + : existing.timeline.addAll(incoming.timeline)), receipts: incoming.receipts.entries.fold( existing.receipts, (receiptAcc, event) => receiptAcc.add( diff --git a/lib/controllers/via_controller.dart b/lib/controllers/via_controller.dart index 0c24487..69e9f4a 100644 --- a/lib/controllers/via_controller.dart +++ b/lib/controllers/via_controller.dart @@ -25,9 +25,10 @@ class ViaController extends Notifier { addUserId(ref.watch(ClientStateController.provider)?.userId); - final powerLevels = room.events.firstWhereOrNull( - (event) => event.rowId == room.state[EventType.powerLevels.type]?[""], - ); + final powerLevelsEventId = room.state[EventType.powerLevels.type]?[""]; + final powerLevels = powerLevelsEventId == null + ? null + : room.events[powerLevelsEventId]; if (powerLevels?.content case PowerLevelsContent(:final users)) { for (final userId in users.keys) { @@ -38,9 +39,10 @@ class ViaController extends Notifier { final members = room.state[EventType.membership.type]?.values.toIList(); for (var i = 0; servers.length < 5; i++) { - final member = room.events.firstWhereOrNull( - (event) => event.rowId == members?.getOrNull(i), - ); + final membershipEventId = members?.getOrNull(i); + final member = membershipEventId == null + ? null + : room.events[membershipEventId]; if (member?.content case MembershipContent(:final status)) { if (status == MembershipStatus.join) { diff --git a/lib/models/room.dart b/lib/models/room.dart index 98dd6da..8aaadbe 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -8,31 +8,48 @@ part "room.g.dart"; @freezed abstract class Room with _$Room { + static IMap timelineTupleJsonToIMap(List json) => + IMap.fromEntries( + json.map( + (timelineTuple) => MapEntry( + timelineTuple["timeline_rowid"], + timelineTuple["event_rowid"], + ), + ), + ); + + static IMap eventsJsonToIMap(List json) => + IMap.fromEntries( + json.map((eventJson) { + final event = Event.fromJson(eventJson); + return MapEntry(event.rowId, event); + }), + ); + + /// [timeline] is an IMap of timelineRowId to eventRowId + /// [events] is an IMap of eventRowId to event const factory Room({ @JsonKey(name: "meta") RoomMetadata? metadata, - @Default(IList.empty()) IList timeline, + @Default(IMap.empty()) + @JsonKey(fromJson: Room.timelineTupleJsonToIMap) + IMap timeline, + + @Default(IMap.empty()) + @JsonKey(fromJson: Room.eventsJsonToIMap) + IMap events, + @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, + @Default(IMap.empty()) IMap> receipts, @Default(false) bool dismissNotifications, @Default(true) bool hasMore, + + // required IMap accountData, // required IList notifications, }) = _Room; factory Room.fromJson(Map json) => _$RoomFromJson(json); } - -@freezed -abstract class TimelineRowTuple with _$TimelineRowTuple { - const factory TimelineRowTuple({ - @JsonKey(name: "timeline_rowid") required int timelineRowId, - @JsonKey(name: "event_rowid") int? eventRowId, - }) = _TimelineRowTuple; - - factory TimelineRowTuple.fromJson(Map json) => - _$TimelineRowTupleFromJson(json); -} diff --git a/lib/widgets/room_chat.dart b/lib/widgets/room_chat.dart index 98135c1..1bcb1ac 100644 --- a/lib/widgets/room_chat.dart +++ b/lib/widgets/room_chat.dart @@ -81,14 +81,15 @@ class RoomChat extends HookConsumerWidget { Future listener() async { if (!scrollController.position.atEdge) return; + final room = ref.watch( + RoomsController.provider.select((value) => value[roomId]), + ); + if (room == null) return; + if (scrollController.position.pixels == 0) { - context.mounted; - final room = ref.watch( - RoomsController.provider.select((value) => value[roomId]), - ); - if (room != null) client.markRead(room); + await client.markRead(room); } else { - await notifier.loadOlder(); + if (room.hasMore) await notifier.loadOlder(); } }