Remove flutter chat #26

Manually merged
Henry-Hiles merged 108 commits from remove-flutter-chat into main 2026-05-22 15:26:28 -04:00
10 changed files with 130 additions and 117 deletions
Showing only changes of commit e00cd12bb9 - Show all commits

some performance improvements

Henry Hiles 2026-05-21 11:24:18 -04:00
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs

View file

@ -1,7 +1,7 @@
import "dart:ffi"; import "dart:ffi";
import "dart:io"; import "dart:io";
import "dart:isolate"; import "dart:isolate";
import "package:collection/collection.dart"; import "dart:math";
import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:ffi/ffi.dart"; import "package:ffi/ffi.dart";
import "package:flutter/foundation.dart"; import "package:flutter/foundation.dart";
@ -237,9 +237,8 @@ class ClientController extends AsyncNotifier<int> {
_sendCommand("set_membership", request.toJson()); _sendCommand("set_membership", request.toJson());
Future<void> markRead(Room room) async { Future<void> markRead(Room room) async {
final event = room.events.firstWhereOrNull( final eventRowId = room.timeline[room.timeline.keys.reduce(max)];
(event) => event.rowId == room.timeline.last.eventRowId, final event = eventRowId == null ? null : room.events[eventRowId];
);
if (event == null || room.metadata == null) return; if (event == null || room.metadata == null) return;
await _sendCommand("mark_read", { await _sendCommand("mark_read", {

View file

@ -14,7 +14,7 @@ class EventController extends AsyncNotifier<Event?> {
final room = ref.watch( final room = ref.watch(
RoomsController.provider.select((value) => value[request.roomId]), RoomsController.provider.select((value) => value[request.roomId]),
); );
final event = room?.events.firstWhereOrNull( final event = room?.events.values.firstWhereOrNull(
(event) => event.eventId == request.eventId, (event) => event.eventId == request.eventId,
); );

View file

@ -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/content/membership.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
class MembersByStatusController extends AsyncNotifier<IList<Event>> { class MembersByStatusController extends AsyncNotifier<ISet<Event>> {
final MembersByStatusConfig config; final MembersByStatusConfig config;
MembersByStatusController(this.config); MembersByStatusController(this.config);
@override @override
Future<IList<Event>> build() => ref.watch( Future<ISet<Event>> build() => ref.watch(
MembersController.provider(config.roomId).selectAsync( MembersController.provider(config.roomId).selectAsync(
(members) => members (members) => members
.where( .where(
@ -19,14 +19,14 @@ class MembersByStatusController extends AsyncNotifier<IList<Event>> {
_ => false, _ => false,
}, },
) )
.toIList(), .toISet(),
), ),
); );
static final provider = static final provider =
AsyncNotifierProvider.family< AsyncNotifierProvider.family<
MembersByStatusController, MembersByStatusController,
IList<Event>, ISet<Event>,
MembersByStatusConfig MembersByStatusConfig
>(MembersByStatusController.new); >(MembersByStatusController.new);
} }

View file

@ -1,4 +1,3 @@
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";
@ -7,17 +6,17 @@ import "package:nexus/models/content/content.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/requests/get_room_state_request.dart"; import "package:nexus/models/requests/get_room_state_request.dart";
class MembersController extends AsyncNotifier<IList<Event>> { class MembersController extends AsyncNotifier<ISet<Event>> {
final String roomId; final String roomId;
MembersController(this.roomId); MembersController(this.roomId);
@override @override
Future<IList<Event>> build() async { Future<ISet<Event>> build() async {
final room = ref.watch( final room = ref.watch(
RoomsController.provider.select((value) => value[roomId]), RoomsController.provider.select((value) => value[roomId]),
); );
if (room == null) return const IList.empty(); if (room == null) return const ISet.empty();
if (!room.hasFetchedMembers) { if (!room.hasFetchedMembers) {
final fetchedState = await ref final fetchedState = await ref
@ -30,21 +29,18 @@ class MembersController extends AsyncNotifier<IList<Event>> {
), ),
); );
ref await ref
.read(RoomsController.provider.notifier) .read(RoomsController.provider.notifier)
.addState(roomId, fetchedState, isMembers: true); .addState(roomId, fetchedState, isMembers: true);
} }
return room.state[EventType.membership.type]?.values return room.state[EventType.membership.type]?.values
.map( .map((rowId) => room.events[rowId])
(rowId) =>
room.events.firstWhereOrNull((event) => event.rowId == rowId),
)
.nonNulls .nonNulls
.toIList() ?? .toISet() ??
const IList.empty(); const ISet.empty();
} }
static final provider = AsyncNotifierProvider.autoDispose static final provider = AsyncNotifierProvider.autoDispose
.family<MembersController, IList<Event>, String>(MembersController.new); .family<MembersController, ISet<Event>, String>(MembersController.new);
} }

View file

@ -1,4 +1,3 @@
import "package:collection/collection.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/rooms_controller.dart";
@ -24,9 +23,9 @@ class PowerLevelController extends Notifier<bool> {
RoomsController.provider.select((value) => value[config.roomId]), RoomsController.provider.select((value) => value[config.roomId]),
); );
final event = room?.events.firstWhereOrNull( final eventRowId = room?.state[EventType.powerLevels.type]?[""];
(event) => event.rowId == room.state[EventType.powerLevels.type]?[""],
); final event = eventRowId == null ? null : room?.events[eventRowId];
final content = event?.content is PowerLevelsContent final content = event?.content is PowerLevelsContent
? event!.content ? event!.content
: PowerLevelsContent(); : PowerLevelsContent();

View file

@ -1,4 +1,5 @@
import "dart:async"; import "dart:async";
import "dart:math";
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";
@ -33,7 +34,7 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
GetRoomStateRequest(roomId: roomId), 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. // 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<IList<Event>> {
loadOlder(); loadOlder();
} }
return room.timeline.reversed return room.timeline
.map((timeline) { .toEntryIList(compare: (a, b) => (a?.key ?? 0).compareTo(b?.key ?? 0))
final foundEvent = room.events.firstWhereOrNull( .map((entry) {
(event) => event.rowId == timeline.eventRowId, 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 ? null
: room.events.firstWhereOrNull( : room.events[foundEvent.lastEditRowId];
(event) => event.rowId == foundEvent?.lastEditRowId,
);
return foundEvent?.copyWith( return editedEvent == null
content: editedEvent?.content ?? foundEvent.content, ? foundEvent
); : foundEvent?.copyWith(content: editedEvent.content);
}) })
.nonNulls .nonNulls
.toIList(); .toIList();
@ -72,34 +73,37 @@ class RoomChatController extends AsyncNotifier<IList<Event>> {
); );
Future<bool> loadOlder() async { Future<bool> loadOlder() async {
final timelineKeys = ref
.read(RoomsController.provider.select((value) => value[roomId]))
?.timeline
.keys;
final response = await ref final response = await ref
.watch(ClientController.provider.notifier) .watch(ClientController.provider.notifier)
.paginate( .paginate(
PaginateRequest( PaginateRequest(
roomId: roomId, roomId: roomId,
maxTimelineId: ref maxTimelineId: timelineKeys?.isNotEmpty == true
.read(RoomsController.provider.select((value) => value[roomId])) ? timelineKeys?.reduce(min)
?.timeline : null,
.firstOrNull
?.timelineRowId,
), ),
); );
ref await ref
.watch(RoomsController.provider.notifier) .watch(RoomsController.provider.notifier)
.update( .update(
IMap({ IMap({
roomId: Room( 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, hasMore: response.hasMore,
timeline: response.events timeline: IMap.fromIterable(
.map( response.events,
(event) => TimelineRowTuple( keyMapper: (event) => event.timelineRowId,
timelineRowId: event.timelineRowId, valueMapper: (event) => event.rowId,
eventRowId: event.rowId, ),
),
)
.toIList(),
), ),
}), }),
const ISet.empty(), const ISet.empty(),

View file

@ -1,4 +1,3 @@
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/event.dart";
@ -9,47 +8,49 @@ 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}) => Future<void> addState(
update( String roomId,
{ IList<Event> state, {
roomId: Room( bool isMembers = false,
events: state, }) => update(
hasFetchedState: true, {
hasFetchedMembers: isMembers, roomId: Room(
state: state.fold( events: IMap.fromEntries(
const IMap.empty(), state.map((event) => MapEntry(event.rowId, event)),
(previousValue, stateEvent) => previousValue.add( ),
stateEvent.type, hasFetchedState: true,
(previousValue[stateEvent.type] ?? const IMap.empty()).addAll( hasFetchedMembers: isMembers,
IMap({ state: state.fold(
if (stateEvent.stateKey != null) const IMap.empty(),
stateEvent.stateKey!: stateEvent.rowId, (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<String, Room> rooms, ISet<String> leftRooms) { Future<void> update(IMap<String, Room> rooms, ISet<String> leftRooms) async {
final merged = rooms.entries.fold(state, (acc, entry) { final merged = rooms.entries.fold(state, (acc, entry) {
final roomId = entry.key; final roomId = entry.key;
final incoming = entry.value; final incoming = entry.value;
final existing = acc[roomId]; final existing = acc[roomId];
final events = existing?.events.updateById(
incoming.events,
(item) => item.eventId,
);
return acc.add( return acc.add(
roomId, roomId,
existing?.copyWith( existing?.copyWith(
hasMore: incoming.hasMore, hasMore: incoming.hasMore,
metadata: incoming.metadata ?? existing.metadata, metadata: incoming.metadata ?? existing.metadata,
events: events!, events: incoming.events.isEmpty
? existing.events
: existing.events.addAll(incoming.events),
state: incoming.state.entries.fold( state: incoming.state.entries.fold(
existing.state, existing.state,
(previousValue, event) => previousValue.add( (previousValue, event) => previousValue.add(
@ -64,15 +65,9 @@ class RoomsController extends Notifier<IMap<String, Room>> {
incoming.hasFetchedMembers || existing.hasFetchedMembers, incoming.hasFetchedMembers || existing.hasFetchedMembers,
hasFetchedState: hasFetchedState:
incoming.hasFetchedState || existing.hasFetchedState, incoming.hasFetchedState || existing.hasFetchedState,
timeline: timeline: (incoming.reset
(incoming.reset ? incoming.timeline
? incoming.timeline : existing.timeline.addAll(incoming.timeline)),
: existing.timeline.updateById(
incoming.timeline,
(item) => item.timelineRowId,
))
.sortedBy((element) => element.timelineRowId)
.toIList(),
receipts: incoming.receipts.entries.fold( receipts: incoming.receipts.entries.fold(
existing.receipts, existing.receipts,
(receiptAcc, event) => receiptAcc.add( (receiptAcc, event) => receiptAcc.add(

View file

@ -25,9 +25,10 @@ class ViaController extends Notifier<String> {
addUserId(ref.watch(ClientStateController.provider)?.userId); addUserId(ref.watch(ClientStateController.provider)?.userId);
final powerLevels = room.events.firstWhereOrNull( final powerLevelsEventId = room.state[EventType.powerLevels.type]?[""];
(event) => event.rowId == room.state[EventType.powerLevels.type]?[""], final powerLevels = powerLevelsEventId == null
); ? null
: room.events[powerLevelsEventId];
if (powerLevels?.content case PowerLevelsContent(:final users)) { if (powerLevels?.content case PowerLevelsContent(:final users)) {
for (final userId in users.keys) { for (final userId in users.keys) {
@ -38,9 +39,10 @@ class ViaController extends Notifier<String> {
final members = room.state[EventType.membership.type]?.values.toIList(); final members = room.state[EventType.membership.type]?.values.toIList();
for (var i = 0; servers.length < 5; i++) { for (var i = 0; servers.length < 5; i++) {
final member = room.events.firstWhereOrNull( final membershipEventId = members?.getOrNull(i);
(event) => event.rowId == members?.getOrNull(i), final member = membershipEventId == null
); ? null
: room.events[membershipEventId];
if (member?.content case MembershipContent(:final status)) { if (member?.content case MembershipContent(:final status)) {
if (status == MembershipStatus.join) { if (status == MembershipStatus.join) {

View file

@ -8,31 +8,48 @@ part "room.g.dart";
@freezed @freezed
abstract class Room with _$Room { abstract class Room with _$Room {
static IMap<int, int?> timelineTupleJsonToIMap(List<dynamic> json) =>
IMap.fromEntries(
json.map(
(timelineTuple) => MapEntry(
timelineTuple["timeline_rowid"],
timelineTuple["event_rowid"],
),
),
);
static IMap<int, Event> eventsJsonToIMap(List<dynamic> 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({ const factory Room({
@JsonKey(name: "meta") RoomMetadata? metadata, @JsonKey(name: "meta") RoomMetadata? metadata,
@Default(IList.empty()) IList<TimelineRowTuple> timeline, @Default(IMap.empty())
@JsonKey(fromJson: Room.timelineTupleJsonToIMap)
IMap<int, int?> timeline,
@Default(IMap.empty())
@JsonKey(fromJson: Room.eventsJsonToIMap)
IMap<int, Event> events,
@Default(false) bool reset, @Default(false) bool reset,
@Default(false) bool hasFetchedState, @Default(false) bool hasFetchedState,
@Default(false) bool hasFetchedMembers, @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,
@Default(IList.empty()) IList<Event> events,
@Default(IMap.empty()) IMap<String, IList<ReadReceipt>> receipts, @Default(IMap.empty()) IMap<String, IList<ReadReceipt>> receipts,
@Default(false) bool dismissNotifications, @Default(false) bool dismissNotifications,
@Default(true) bool hasMore, @Default(true) bool hasMore,
// required IMap<String, AccountData> accountData,
// required IList<Notification> notifications, // required IList<Notification> notifications,
}) = _Room; }) = _Room;
factory Room.fromJson(Map<String, Object?> json) => _$RoomFromJson(json); factory Room.fromJson(Map<String, Object?> 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<String, Object?> json) =>
_$TimelineRowTupleFromJson(json);
}

View file

@ -81,14 +81,15 @@ class RoomChat extends HookConsumerWidget {
Future<void> listener() async { Future<void> listener() async {
if (!scrollController.position.atEdge) return; if (!scrollController.position.atEdge) return;
final room = ref.watch(
RoomsController.provider.select((value) => value[roomId]),
);
if (room == null) return;
if (scrollController.position.pixels == 0) { if (scrollController.position.pixels == 0) {
context.mounted; await client.markRead(room);
final room = ref.watch(
RoomsController.provider.select((value) => value[roomId]),
);
if (room != null) client.markRead(room);
} else { } else {
await notifier.loadOlder(); if (room.hasMore) await notifier.loadOlder();
} }
} }