Gomuks SDK Rewrite #2

Closed
Henry-Hiles wants to merge 34 commits from go into main
12 changed files with 182 additions and 156 deletions
Showing only changes of commit c084bc4caf - Show all commits
Henry Hiles 2026-01-27 01:13:02 +00:00
No known key found for this signature in database

View file

@ -1,11 +1,12 @@
import "dart:convert";
import "dart:developer"; import "dart:developer";
import "dart:ffi"; import "dart:ffi";
import "dart:isolate"; import "dart:isolate";
import "package:ffi/ffi.dart"; import "package:ffi/ffi.dart";
import "package:flutter/foundation.dart"; import "package:flutter/foundation.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/sync_status_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/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/models/client_state.dart"; import "package:nexus/models/client_state.dart";
import "package:nexus/models/login.dart"; import "package:nexus/models/login.dart";
@ -18,55 +19,64 @@ class ClientController extends AsyncNotifier<int> {
@override @override
Future<int> build() async { Future<int> build() async {
final handle = await Isolate.run(GomuksInit); final handle = await Isolate.run(GomuksInit);
ref.onDispose(() => GomuksDestroy(handle));
final errorCode = GomuksStart( final callable =
handle, NativeCallable<
NativeCallable< Void Function(Pointer<Char>, Int64, GomuksOwnedBuffer)
Void Function(Pointer<Char>, Int64, GomuksOwnedBuffer) >.listener((
>.listener(( Pointer<Char> command,
Pointer<Char> command, int requestId,
int requestId, GomuksOwnedBuffer data,
GomuksOwnedBuffer data, ) {
) { try {
try { final muksEventType = command.cast<Utf8>().toDartString();
final muksEventType = command.cast<Utf8>().toDartString(); debugPrint("Handling $muksEventType...");
debugPrint("Handling $muksEventType..."); final Map<String, dynamic> decodedMuksEvent = data.toJson();
final Map<String, dynamic> decodedMuksEvent = data.toJson();
switch (muksEventType) { switch (muksEventType) {
case "client_state": case "client_state":
ref ref
.watch(ClientStateController.provider.notifier) .watch(ClientStateController.provider.notifier)
.set(ClientState.fromJson(decodedMuksEvent)); .set(ClientState.fromJson(decodedMuksEvent));
break; break;
case "sync_status": case "sync_status":
ref ref
.watch(SyncStatusController.provider.notifier) .watch(SyncStatusController.provider.notifier)
.set(SyncStatus.fromJson(decodedMuksEvent)); .set(SyncStatus.fromJson(decodedMuksEvent));
break; break;
case "sync_complete": case "sync_complete":
final syncData = SyncData.fromJson(decodedMuksEvent); final syncData = SyncData.fromJson(decodedMuksEvent);
debugPrint(jsonEncode(syncData.toJson())); final roomProvider = RoomsController.provider;
// ref if (syncData.clearState) ref.invalidate(roomProvider);
// .watch(SyncStatusController.provider.notifier) ref
// .set(SyncStatus.fromJson(decodedMuksEvent)); .watch(roomProvider.notifier)
break; .update(syncData.rooms, syncData.leftRooms);
case "typing": ref
//TODO: IMPL .watch(TopLevelSpacesController.provider.notifier)
break; .set(syncData.topLevelSpaces);
default:
debugPrint("Unhandled event: $muksEventType"); // ref
} // .watch(SyncStatusController.provider.notifier)
debugPrint("Finished handling $muksEventType..."); // .set(SyncStatus.fromJson(decodedMuksEvent));
} catch (error, stackTrace) { break;
debugger(); case "typing":
debugPrintStack(stackTrace: stackTrace, label: error.toString()); //TODO: IMPL
break;
default:
debugPrint("Unhandled event: $muksEventType");
} }
}) debugPrint("Finished handling $muksEventType...");
.nativeFunction, } 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; if (errorCode == 0) return handle;
throw Exception("GomuksStart returned error code $errorCode"); throw Exception("GomuksStart returned error code $errorCode");

View file

@ -1,23 +1,57 @@
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/models/read_receipt.dart";
import "package:nexus/helpers/extensions/get_full_room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/models/full_room.dart";
class RoomsController extends AsyncNotifier<IList<FullRoom>> { class RoomsController extends Notifier<IMap<String, Room>> {
@override @override
Future<IList<FullRoom>> build() async { IMap<String, Room> build() => const IMap.empty();
final client = await ref.watch(ClientController.provider.future);
ref.onDispose( void update(IMap<String, Room> rooms, ISet<String> leftRooms) {
client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel, 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<dynamic, dynamic>()).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<ReadReceipt>()).addAll(
event.value,
),
),
),
) ??
incoming,
);
});
final prunedList = leftRooms.fold(
merged,
(acc, roomId) => acc.remove(roomId),
); );
state = prunedList;
return IList(await Future.wait(client.rooms.map((room) => room.fullRoom)));
} }
static final provider = static final provider = NotifierProvider<RoomsController, IMap<String, Room>>(
AsyncNotifierProvider<RoomsController, IList<FullRoom>>( RoomsController.new,
RoomsController.new, );
);
} }

View file

@ -2,72 +2,50 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/material.dart"; import "package:flutter/material.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/helpers/extensions/get_full_room.dart"; import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/helpers/extensions/room_to_children.dart"; import "package:nexus/controllers/top_level_spaces_controller.dart";
import "package:nexus/models/space.dart"; import "package:nexus/models/space.dart";
class SpacesController extends AsyncNotifier<IList<Space>> { class SpacesController extends AsyncNotifier<IList<Space>> {
@override @override
Future<IList<Space>> build() async { Future<IList<Space>> build() async {
final client = await ref.watch(ClientController.provider.future); final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider);
final rooms = ref.watch(RoomsController.provider);
ref.onDispose( final topLevelSpaces = topLevelSpaceIds
client.onSync.stream.listen((_) => ref.invalidateSelf()).cancel, .map((id) => rooms[id])
); .nonNulls
.toIList();
final topLevel = IList( final dmRooms = rooms.values
await Future.wait( .where((room) => room.metadata?.dmUserId != null)
client.rooms .toIList();
.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 topLevelSpaces = topLevel.where((r) => r.roomData.isSpace).toIList(); final topLevelRooms = rooms.values
final topLevelRooms = topLevel.where((r) => !r.roomData.isSpace).toIList(); .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([ return IList([
Space( Space(
client: client,
title: "Home",
id: "home", id: "home",
title: "Home",
children: topLevelRooms, children: topLevelRooms,
icon: Icons.home, icon: Icons.home,
), ),
Space( Space(
client: client,
title: "Direct Messages",
id: "dms", id: "dms",
children: IList( title: "Direct Messages",
await Future.wait( children: dmRooms,
client.rooms
.where((room) => room.isDirectChat)
.map((room) => room.fullRoom),
),
),
icon: Icons.person, icon: Icons.person,
), ),
...(await Future.wait( ...topLevelSpaces,
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()),
),
),
)),
]); ]);
} }

View file

@ -0,0 +1,14 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
class TopLevelSpacesController extends Notifier<IList<String>> {
@override
IList<String> build() => const IList.empty();
void set(IList<String> newSpaces) => state = newSpaces;
static final provider =
NotifierProvider<TopLevelSpacesController, IList<String>>(
TopLevelSpacesController.new,
);
}

View file

@ -1,5 +0,0 @@
import "package:matrix/matrix.dart";
extension GetHeaders on Client {
Map<String, String> get headers => {"authorization": "Bearer $accessToken"};
}

View file

@ -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<IList<FullRoom>> 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();
}
}

View file

@ -1,3 +1,4 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart"; import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/epoch_date_time_converter.dart"; import "package:nexus/models/epoch_date_time_converter.dart";
part "event.freezed.dart"; part "event.freezed.dart";
@ -14,10 +15,10 @@ abstract class Event with _$Event {
required String type, required String type,
String? stateKey, String? stateKey,
@EpochDateTimeConverter() required DateTime timestamp, @EpochDateTimeConverter() required DateTime timestamp,
required Map<String, dynamic> content, required IMap<String, dynamic> content,
Map<String, dynamic>? decrypted, IMap<String, dynamic>? decrypted,
String? decryptedType, String? decryptedType,
@Default({}) Map<String, dynamic> unsigned, @Default(IMap.empty()) IMap<String, dynamic> unsigned,
LocalContent? localContent, LocalContent? localContent,
String? transactionId, String? transactionId,
String? redactedBy, String? redactedBy,
@ -25,7 +26,7 @@ abstract class Event with _$Event {
String? relatesType, String? relatesType,
String? decryptionError, String? decryptionError,
String? sendError, String? sendError,
@Default({}) Map<String, int> reactions, @Default(IMap.empty()) IMap<String, int> reactions,
int? lastEditRowId, int? lastEditRowId,
@UnreadTypeConverter() UnreadType? unreadType, @UnreadTypeConverter() UnreadType? unreadType,
}) = _Event; }) = _Event;
@ -57,6 +58,7 @@ class UnreadTypeConverter implements JsonConverter<UnreadType?, int?> {
int? toJson(UnreadType? object) => object?.value; int? toJson(UnreadType? object) => object?.value;
} }
// I think this is correct but I'm not sure, its some type of bitmask.
@immutable @immutable
class UnreadType { class UnreadType {
final int value; final int value;

View file

@ -1,3 +1,4 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart"; import "package:freezed_annotation/freezed_annotation.dart";
part "lazy_load_summary.freezed.dart"; part "lazy_load_summary.freezed.dart";
part "lazy_load_summary.g.dart"; part "lazy_load_summary.g.dart";
@ -5,7 +6,7 @@ part "lazy_load_summary.g.dart";
@freezed @freezed
abstract class LazyLoadSummary with _$LazyLoadSummary { abstract class LazyLoadSummary with _$LazyLoadSummary {
const factory LazyLoadSummary({ const factory LazyLoadSummary({
required List<String>? heroes, required IList<String>? heroes,
required int? joinedMemberCount, required int? joinedMemberCount,
required int? invitedMemberCount, required int? invitedMemberCount,
}) = _LazyLoadSummary; }) = _LazyLoadSummary;

View file

@ -1,3 +1,4 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart"; import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
import "package:nexus/models/read_receipt.dart"; import "package:nexus/models/read_receipt.dart";
@ -9,14 +10,14 @@ part "room.g.dart";
abstract class Room with _$Room { abstract class Room with _$Room {
const factory Room({ const factory Room({
@JsonKey(name: "meta") RoomMetadata? metadata, @JsonKey(name: "meta") RoomMetadata? metadata,
@Default([]) List<TimelineRowTuple> timeline, @Default(IList.empty()) IList<TimelineRowTuple> timeline,
required bool reset, required bool reset,
required Map<String, Map> state, required IMap<String, IMap> state,
// required Map<String, AccountData> accountData, // required IMap<String, AccountData> accountData,
required List<Event> events, required IList<Event> events,
@Default({}) Map<String, List<ReadReceipt>> receipts, @Default(IMap.empty()) IMap<String, IList<ReadReceipt>> receipts,
required bool dismissNotifications, required bool dismissNotifications,
// required List<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);

View file

@ -8,10 +8,12 @@ part "room_metadata.g.dart";
abstract class RoomMetadata with _$RoomMetadata { abstract class RoomMetadata with _$RoomMetadata {
const factory RoomMetadata({ const factory RoomMetadata({
@JsonKey(name: "room_id") required String id, @JsonKey(name: "room_id") required String id,
// required CreateEventContent creationContent, // required CreateEventContent creationContent,
// required TombstoneEventContent tombstoneEventContent, // required TombstoneEventContent tombstoneEventContent,
String? name, String? name,
Uri? avatar, Uri? avatar,
String? dmUserId,
String? topic, String? topic,
String? canonicalAlias, String? canonicalAlias,
LazyLoadSummary? lazyLoadSummary, LazyLoadSummary? lazyLoadSummary,

15
lib/models/space.dart Normal file
View file

@ -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<Room> children,
}) = _Space;
}

View file

@ -1,3 +1,4 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:freezed_annotation/freezed_annotation.dart"; import "package:freezed_annotation/freezed_annotation.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
part "sync_data.freezed.dart"; part "sync_data.freezed.dart";
@ -7,12 +8,12 @@ part "sync_data.g.dart";
abstract class SyncData with _$SyncData { abstract class SyncData with _$SyncData {
const factory SyncData({ const factory SyncData({
@Default(false) bool clearState, @Default(false) bool clearState,
// required Map<String, AccountData> accountData, // required IMap<String, AccountData> accountData,
@Default({}) Map<String, Room> rooms, @Default(IMap.empty()) IMap<String, Room> rooms,
@Default([]) List<String> leftRooms, @Default(ISet.empty()) ISet<String> leftRooms,
// required List<InvitedRoom> invitedRooms, // required IList<InvitedRoom> invitedRooms,
// required List<SpaceEdge> spaceEdges, // required IList<SpaceEdge> spaceEdges,
@Default([]) List<String> topLevelSpaces, @Default(IList.empty()) IList<String> topLevelSpaces,
}) = _SyncData; }) = _SyncData;
factory SyncData.fromJson(Map<String, Object?> json) => factory SyncData.fromJson(Map<String, Object?> json) =>