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: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<int> {
_sendCommand("set_membership", request.toJson());
Future<void> 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", {

View file

@ -14,7 +14,7 @@ class EventController extends AsyncNotifier<Event?> {
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,
);

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

View file

@ -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<IList<Event>> {
class MembersController extends AsyncNotifier<ISet<Event>> {
final String roomId;
MembersController(this.roomId);
@override
Future<IList<Event>> build() async {
Future<ISet<Event>> 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<IList<Event>> {
),
);
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<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:nexus/controllers/client_state_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]),
);
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();

View file

@ -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<IList<Event>> {
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<IList<Event>> {
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<IList<Event>> {
);
Future<bool> 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(),

View file

@ -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<IMap<String, Room>> {
@override
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,
}),
),
),
Future<void> addState(
String roomId,
IList<Event> 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<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 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<IMap<String, Room>> {
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(

View file

@ -25,9 +25,10 @@ class ViaController extends Notifier<String> {
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<String> {
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) {

View file

@ -8,31 +8,48 @@ part "room.g.dart";
@freezed
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({
@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 hasFetchedState,
@Default(false) bool hasFetchedMembers,
@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(false) bool dismissNotifications,
@Default(true) bool hasMore,
// required IMap<String, AccountData> accountData,
// required IList<Notification> notifications,
}) = _Room;
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 {
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();
}
}