From f51d773885a3e1132331db8f638cb27ebd911254 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Tue, 27 Jan 2026 01:13:02 +0000 Subject: [PATCH] wip --- lib/controllers/client_controller.dart | 102 ++++++++++-------- lib/controllers/rooms_controller.dart | 62 ++++++++--- lib/controllers/spaces_controller.dart | 72 +++++-------- .../top_level_spaces_controller.dart | 14 +++ lib/helpers/extensions/get_headers.dart | 5 - lib/helpers/extensions/room_to_children.dart | 27 ----- lib/models/event.dart | 10 +- lib/models/lazy_load_summary.dart | 3 +- lib/models/room.dart | 13 +-- lib/models/room_metadata.dart | 2 + lib/models/space.dart | 15 +++ lib/models/sync_data.dart | 13 +-- 12 files changed, 182 insertions(+), 156 deletions(-) create mode 100644 lib/controllers/top_level_spaces_controller.dart delete mode 100644 lib/helpers/extensions/get_headers.dart delete mode 100644 lib/helpers/extensions/room_to_children.dart create mode 100644 lib/models/space.dart diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index cd4aff2..2ebb772 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -1,11 +1,12 @@ -import "dart:convert"; import "dart:developer"; import "dart:ffi"; import "dart:isolate"; import "package:ffi/ffi.dart"; import "package:flutter/foundation.dart"; import "package:nexus/controllers/client_state_controller.dart"; +import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/sync_status_controller.dart"; +import "package:nexus/controllers/top_level_spaces_controller.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/models/client_state.dart"; import "package:nexus/models/login.dart"; @@ -18,55 +19,64 @@ class ClientController extends AsyncNotifier { @override Future build() async { final handle = await Isolate.run(GomuksInit); - ref.onDispose(() => GomuksDestroy(handle)); - final errorCode = GomuksStart( - handle, - NativeCallable< - Void Function(Pointer, Int64, GomuksOwnedBuffer) - >.listener(( - Pointer command, - int requestId, - GomuksOwnedBuffer data, - ) { - try { - final muksEventType = command.cast().toDartString(); - debugPrint("Handling $muksEventType..."); - final Map decodedMuksEvent = data.toJson(); + final callable = + NativeCallable< + Void Function(Pointer, Int64, GomuksOwnedBuffer) + >.listener(( + Pointer command, + int requestId, + GomuksOwnedBuffer data, + ) { + try { + final muksEventType = command.cast().toDartString(); + debugPrint("Handling $muksEventType..."); + final Map decodedMuksEvent = data.toJson(); - switch (muksEventType) { - case "client_state": - ref - .watch(ClientStateController.provider.notifier) - .set(ClientState.fromJson(decodedMuksEvent)); - break; - case "sync_status": - ref - .watch(SyncStatusController.provider.notifier) - .set(SyncStatus.fromJson(decodedMuksEvent)); - break; - case "sync_complete": - final syncData = SyncData.fromJson(decodedMuksEvent); - debugPrint(jsonEncode(syncData.toJson())); + switch (muksEventType) { + case "client_state": + ref + .watch(ClientStateController.provider.notifier) + .set(ClientState.fromJson(decodedMuksEvent)); + break; + case "sync_status": + ref + .watch(SyncStatusController.provider.notifier) + .set(SyncStatus.fromJson(decodedMuksEvent)); + break; + case "sync_complete": + final syncData = SyncData.fromJson(decodedMuksEvent); + final roomProvider = RoomsController.provider; - // ref - // .watch(SyncStatusController.provider.notifier) - // .set(SyncStatus.fromJson(decodedMuksEvent)); - break; - case "typing": - //TODO: IMPL - break; - default: - debugPrint("Unhandled event: $muksEventType"); - } - debugPrint("Finished handling $muksEventType..."); - } catch (error, stackTrace) { - debugger(); - debugPrintStack(stackTrace: stackTrace, label: error.toString()); + if (syncData.clearState) ref.invalidate(roomProvider); + ref + .watch(roomProvider.notifier) + .update(syncData.rooms, syncData.leftRooms); + ref + .watch(TopLevelSpacesController.provider.notifier) + .set(syncData.topLevelSpaces); + + // ref + // .watch(SyncStatusController.provider.notifier) + // .set(SyncStatus.fromJson(decodedMuksEvent)); + break; + case "typing": + //TODO: IMPL + break; + default: + debugPrint("Unhandled event: $muksEventType"); } - }) - .nativeFunction, - ); + debugPrint("Finished handling $muksEventType..."); + } catch (error, stackTrace) { + debugger(); + debugPrintStack(stackTrace: stackTrace, label: error.toString()); + } + }); + + ref.onDispose(() => GomuksDestroy(handle)); + ref.onDispose(callable.close); + + final errorCode = GomuksStart(handle, callable.nativeFunction); if (errorCode == 0) return handle; throw Exception("GomuksStart returned error code $errorCode"); diff --git a/lib/controllers/rooms_controller.dart b/lib/controllers/rooms_controller.dart index 864d656..0a41f31 100644 --- a/lib/controllers/rooms_controller.dart +++ b/lib/controllers/rooms_controller.dart @@ -1,23 +1,57 @@ 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/helpers/extensions/get_full_room.dart"; -import "package:nexus/models/full_room.dart"; +import "package:nexus/models/read_receipt.dart"; +import "package:nexus/models/room.dart"; -class RoomsController extends AsyncNotifier> { +class RoomsController extends Notifier> { @override - Future> build() async { - final client = await ref.watch(ClientController.provider.future); + IMap build() => const IMap.empty(); - ref.onDispose( - client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel, + void update(IMap rooms, ISet leftRooms) { + final merged = rooms.entries.fold(state, (acc, entry) { + final roomId = entry.key; + final incoming = entry.value; + final existing = acc[roomId]; + + return acc.add( + roomId, + existing?.copyWith( + metadata: incoming.metadata ?? existing.metadata, + events: existing.events.addAll(incoming.events), + state: incoming.state.entries.fold( + existing.state, + (stateAcc, event) => stateAcc.add( + event.key, + (stateAcc[event.key] ?? IMap()).addAll( + event.value, + ), + ), + ), + timeline: incoming.reset + ? incoming.timeline + : existing.timeline.addAll(incoming.timeline), + receipts: incoming.receipts.entries.fold( + existing.receipts, + (receiptAcc, event) => receiptAcc.add( + event.key, + (receiptAcc[event.key] ?? IList()).addAll( + event.value, + ), + ), + ), + ) ?? + incoming, + ); + }); + + final prunedList = leftRooms.fold( + merged, + (acc, roomId) => acc.remove(roomId), ); - - return IList(await Future.wait(client.rooms.map((room) => room.fullRoom))); + state = prunedList; } - static final provider = - AsyncNotifierProvider>( - RoomsController.new, - ); + static final provider = NotifierProvider>( + RoomsController.new, + ); } diff --git a/lib/controllers/spaces_controller.dart b/lib/controllers/spaces_controller.dart index 408dc00..e269584 100644 --- a/lib/controllers/spaces_controller.dart +++ b/lib/controllers/spaces_controller.dart @@ -2,72 +2,50 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/helpers/extensions/get_full_room.dart"; -import "package:nexus/helpers/extensions/room_to_children.dart"; +import "package:nexus/controllers/rooms_controller.dart"; +import "package:nexus/controllers/top_level_spaces_controller.dart"; import "package:nexus/models/space.dart"; class SpacesController extends AsyncNotifier> { @override Future> build() async { - final client = await ref.watch(ClientController.provider.future); + final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider); + final rooms = ref.watch(RoomsController.provider); - ref.onDispose( - client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel, - ); + final topLevelSpaces = topLevelSpaceIds + .map((id) => rooms[id]) + .nonNulls + .toIList(); - final topLevel = IList( - await Future.wait( - client.rooms - .where((room) => !room.isDirectChat) - .where( - (room) => client.rooms - .where((room) => room.isSpace) - .every( - (match) => match.spaceChildren.every( - (child) => child.roomId != room.id, - ), - ), - ) - .map((room) => room.fullRoom), - ), - ); + final dmRooms = rooms.values + .where((room) => room.metadata?.dmUserId != null) + .toIList(); - final topLevelSpaces = topLevel.where((r) => r.roomData.isSpace).toIList(); - final topLevelRooms = topLevel.where((r) => !r.roomData.isSpace).toIList(); + final topLevelRooms = rooms.values + .where((room) => room.metadata?.dmUserId == null) + .where( + (room) => spaceRooms.every( + (space) => + space.spaceChildren.every((child) => child.roomId != room.id), + ), + ) + .toIList(); + // 4️⃣ Combine all into a single IList return IList([ Space( - client: client, - title: "Home", id: "home", + title: "Home", children: topLevelRooms, icon: Icons.home, ), Space( - client: client, - title: "Direct Messages", id: "dms", - children: IList( - await Future.wait( - client.rooms - .where((room) => room.isDirectChat) - .map((room) => room.fullRoom), - ), - ), + title: "Direct Messages", + children: dmRooms, icon: Icons.person, ), - ...(await Future.wait( - topLevelSpaces.map( - (space) async => Space( - client: client, - title: space.title, - avatar: space.avatar, - id: space.roomData.id, - roomData: space.roomData, - children: IList(await space.roomData.getAllChildren()), - ), - ), - )), + ...topLevelSpaces, ]); } diff --git a/lib/controllers/top_level_spaces_controller.dart b/lib/controllers/top_level_spaces_controller.dart new file mode 100644 index 0000000..e1f9c88 --- /dev/null +++ b/lib/controllers/top_level_spaces_controller.dart @@ -0,0 +1,14 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +class TopLevelSpacesController extends Notifier> { + @override + IList build() => const IList.empty(); + + void set(IList newSpaces) => state = newSpaces; + + static final provider = + NotifierProvider>( + TopLevelSpacesController.new, + ); +} diff --git a/lib/helpers/extensions/get_headers.dart b/lib/helpers/extensions/get_headers.dart deleted file mode 100644 index b8b1fde..0000000 --- a/lib/helpers/extensions/get_headers.dart +++ /dev/null @@ -1,5 +0,0 @@ -import "package:matrix/matrix.dart"; - -extension GetHeaders on Client { - Map get headers => {"authorization": "Bearer $accessToken"}; -} diff --git a/lib/helpers/extensions/room_to_children.dart b/lib/helpers/extensions/room_to_children.dart deleted file mode 100644 index d115f9a..0000000 --- a/lib/helpers/extensions/room_to_children.dart +++ /dev/null @@ -1,27 +0,0 @@ -import "package:collection/collection.dart"; -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:matrix/matrix.dart"; -import "package:nexus/helpers/extensions/get_full_room.dart"; -import "package:nexus/models/full_room.dart"; - -extension RoomToChildren on Room { - Future> getAllChildren() async { - final direct = await Future.wait( - spaceChildren - .map( - (child) => client.rooms - .firstWhereOrNull((r) => r.id == child.roomId) - ?.fullRoom, - ) - .nonNulls, - ); - - return (await Future.wait( - direct.map( - (child) async => child.roomData.isSpace - ? await child.roomData.getAllChildren() - : [child], - ), - )).expand((list) => list).toIList(); - } -} diff --git a/lib/models/event.dart b/lib/models/event.dart index ce621a3..397c325 100644 --- a/lib/models/event.dart +++ b/lib/models/event.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/epoch_date_time_converter.dart"; part "event.freezed.dart"; @@ -14,10 +15,10 @@ abstract class Event with _$Event { required String type, String? stateKey, @EpochDateTimeConverter() required DateTime timestamp, - required Map content, - Map? decrypted, + required IMap content, + IMap? decrypted, String? decryptedType, - @Default({}) Map unsigned, + @Default(IMap.empty()) IMap unsigned, LocalContent? localContent, String? transactionId, String? redactedBy, @@ -25,7 +26,7 @@ abstract class Event with _$Event { String? relatesType, String? decryptionError, String? sendError, - @Default({}) Map reactions, + @Default(IMap.empty()) IMap reactions, int? lastEditRowId, @UnreadTypeConverter() UnreadType? unreadType, }) = _Event; @@ -57,6 +58,7 @@ class UnreadTypeConverter implements JsonConverter { int? toJson(UnreadType? object) => object?.value; } +// I think this is correct but I'm not sure, its some type of bitmask. @immutable class UnreadType { final int value; diff --git a/lib/models/lazy_load_summary.dart b/lib/models/lazy_load_summary.dart index 5314af8..0cd250f 100644 --- a/lib/models/lazy_load_summary.dart +++ b/lib/models/lazy_load_summary.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; part "lazy_load_summary.freezed.dart"; part "lazy_load_summary.g.dart"; @@ -5,7 +6,7 @@ part "lazy_load_summary.g.dart"; @freezed abstract class LazyLoadSummary with _$LazyLoadSummary { const factory LazyLoadSummary({ - required List? heroes, + required IList? heroes, required int? joinedMemberCount, required int? invitedMemberCount, }) = _LazyLoadSummary; diff --git a/lib/models/room.dart b/lib/models/room.dart index a83daef..91ab952 100644 --- a/lib/models/room.dart +++ b/lib/models/room.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/event.dart"; import "package:nexus/models/read_receipt.dart"; @@ -9,14 +10,14 @@ part "room.g.dart"; abstract class Room with _$Room { const factory Room({ @JsonKey(name: "meta") RoomMetadata? metadata, - @Default([]) List timeline, + @Default(IList.empty()) IList timeline, required bool reset, - required Map state, - // required Map accountData, - required List events, - @Default({}) Map> receipts, + required IMap state, + // required IMap accountData, + required IList events, + @Default(IMap.empty()) IMap> receipts, required bool dismissNotifications, - // required List notifications, + // required IList notifications, }) = _Room; factory Room.fromJson(Map json) => _$RoomFromJson(json); diff --git a/lib/models/room_metadata.dart b/lib/models/room_metadata.dart index 905f220..7636494 100644 --- a/lib/models/room_metadata.dart +++ b/lib/models/room_metadata.dart @@ -8,10 +8,12 @@ part "room_metadata.g.dart"; abstract class RoomMetadata with _$RoomMetadata { const factory RoomMetadata({ @JsonKey(name: "room_id") required String id, + // required CreateEventContent creationContent, // required TombstoneEventContent tombstoneEventContent, String? name, Uri? avatar, + String? dmUserId, String? topic, String? canonicalAlias, LazyLoadSummary? lazyLoadSummary, diff --git a/lib/models/space.dart b/lib/models/space.dart new file mode 100644 index 0000000..15b818c --- /dev/null +++ b/lib/models/space.dart @@ -0,0 +1,15 @@ +import "package:flutter/widgets.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:nexus/models/room.dart"; +part "space.freezed.dart"; + +@freezed +abstract class Space with _$Space { + const factory Space({ + required String id, + required String title, + IconData? icon, + Room? room, + required List children, + }) = _Space; +} diff --git a/lib/models/sync_data.dart b/lib/models/sync_data.dart index 4da57c2..31f69e3 100644 --- a/lib/models/sync_data.dart +++ b/lib/models/sync_data.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/room.dart"; part "sync_data.freezed.dart"; @@ -7,12 +8,12 @@ part "sync_data.g.dart"; abstract class SyncData with _$SyncData { const factory SyncData({ @Default(false) bool clearState, - // required Map accountData, - @Default({}) Map rooms, - @Default([]) List leftRooms, - // required List invitedRooms, - // required List spaceEdges, - @Default([]) List topLevelSpaces, + // required IMap accountData, + @Default(IMap.empty()) IMap rooms, + @Default(ISet.empty()) ISet leftRooms, + // required IList invitedRooms, + // required IList spaceEdges, + @Default(IList.empty()) IList topLevelSpaces, }) = _SyncData; factory SyncData.fromJson(Map json) =>