Separate out GomuksListenerController from ClientController

Fixes #89
This commit is contained in:
Henry Hiles 2026-09-24 11:03:16 -04:00
commit ccca02a0bc
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
6 changed files with 146 additions and 143 deletions

2
gomuks

@ -1 +1 @@
Subproject commit b11eedca121a7081d3ee48165ba363e722d18d87 Subproject commit 480938f9e01e7eae2cf249657efae03df76599d6

View file

@ -6,17 +6,9 @@ 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/cupertino.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
import "package:nexus/controllers/account_data.dart";
import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/init_complete.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/controllers/space_edges.dart";
import "package:nexus/controllers/sync_status.dart";
import "package:nexus/controllers/top_level_spaces.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/main.dart";
import "package:nexus/models/capabilities.dart"; import "package:nexus/models/capabilities.dart";
import "package:nexus/models/content/message.dart"; import "package:nexus/models/content/message.dart";
import "package:nexus/models/event.dart"; import "package:nexus/models/event.dart";
@ -46,9 +38,9 @@ import "package:nexus/models/requests/set_membership.dart";
import "package:nexus/models/requests/set_state.dart"; import "package:nexus/models/requests/set_state.dart";
import "package:nexus/models/requests/upload_media.dart"; import "package:nexus/models/requests/upload_media.dart";
import "package:nexus/models/room.dart"; import "package:nexus/models/room.dart";
import "package:nexus/models/room_metadata.dart";
import "package:nexus/models/room_summary.dart"; import "package:nexus/models/room_summary.dart";
import "package:nexus/models/spec_versions_response.dart"; import "package:nexus/models/spec_versions_response.dart";
import "package:nexus/models/sync_data.dart";
import "package:nexus/src/third_party/gomuks.g.dart"; import "package:nexus/src/third_party/gomuks.g.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:path_provider/path_provider.dart"; import "package:path_provider/path_provider.dart";
@ -56,6 +48,7 @@ import "package:path_provider/path_provider.dart";
class ClientController extends AsyncNotifier<int> { class ClientController extends AsyncNotifier<int> {
@override @override
Future<int> build() async { Future<int> build() async {
debugPrint("Setting Gomuks env...");
if (Platform.isAndroid || Platform.isIOS) { if (Platform.isAndroid || Platform.isIOS) {
final env = { final env = {
"GOMUKS_ROOT": (await getApplicationSupportDirectory()).path, "GOMUKS_ROOT": (await getApplicationSupportDirectory()).path,
@ -70,6 +63,8 @@ class ClientController extends AsyncNotifier<int> {
..free(valuePtr); ..free(valuePtr);
} }
} }
debugPrint("Initializing Gomuks...");
final handle = await Isolate.run(() { final handle = await Isolate.run(() {
final bufferPointer = GomuksConfig( final bufferPointer = GomuksConfig(
matrix: .new( matrix: .new(
@ -85,99 +80,9 @@ class ClientController extends AsyncNotifier<int> {
} }
}); });
final callable =
NativeCallable<
Void Function(Pointer<Char>, Int64, GomuksOwnedBuffer)
>.listener((
Pointer<Char> command,
int requestId,
GomuksOwnedBuffer data,
) {
try {
final muksEventType = command.cast<Utf8>().toDartString();
debugPrint("Handling $muksEventType...");
final decodedMuksEvent = data.toJson();
switch (muksEventType) {
case "client_state":
ref
.watch(ClientStateController.provider.notifier)
.set(.fromJson(decodedMuksEvent));
break;
case "sync_status":
ref
.watch(SyncStatusController.provider.notifier)
.set(.fromJson(decodedMuksEvent));
break;
case "init_complete":
ref.watch(InitCompleteController.provider.notifier).complete();
break;
case "send_complete":
final event = Event.fromJson(decodedMuksEvent["event"]);
ref
.watch(RoomsController.provider.notifier)
.update(
.new({
event.roomId: .new(events: .new({event.rowId: event})),
}),
.new(),
);
break;
case "sync_complete":
final syncData = SyncData.fromJson(decodedMuksEvent);
final roomProvider = RoomsController.provider;
final accountDataProvider = AccountDataController.provider;
if (syncData.clearState) {
ref.invalidate(roomProvider);
ref.invalidate(accountDataProvider);
}
ref
.watch(roomProvider.notifier)
.update(syncData.rooms, syncData.leftRooms);
ref
.watch(accountDataProvider.notifier)
.update(syncData.accountData);
if (syncData.topLevelSpaces != null) {
ref
.watch(TopLevelSpacesController.provider.notifier)
.set(syncData.topLevelSpaces!);
}
if (syncData.spaceEdges != null) {
ref
.watch(SpaceEdgesController.provider.notifier)
.set(syncData.spaceEdges!);
}
// ref
// .watch(SyncStatusController.provider.notifier)
// .set(SyncStatus.fromJson(decodedMuksEvent));
break;
default:
debugPrint("Unhandled event: $muksEventType");
}
debugPrint("Finished handling $muksEventType...");
} catch (error, stackTrace) {
if (kDebugMode) {
debugPrintStack(stackTrace: stackTrace, label: error.toString());
rethrow;
} else {
showError(error, stackTrace);
}
}
});
ref.onDispose(() => GomuksDestroy(handle)); ref.onDispose(() => GomuksDestroy(handle));
ref.onDispose(callable.close);
final errorCode = GomuksStart(handle, callable.nativeFunction); return handle;
if (errorCode == 0) return handle;
throw Exception("GomuksStart returned error code $errorCode");
} }
Future<dynamic> callGomuksMethod( Future<dynamic> callGomuksMethod(
@ -201,13 +106,16 @@ class ClientController extends AsyncNotifier<int> {
return json; return json;
} }
Future<Event?> handlePush(Map<String, dynamic> data) async { Future<(Event, RoomMetadata)> handlePush(Map<String, dynamic> data) async {
final response = await callGomuksMethod( final response = await callGomuksMethod(
data, data,
(handle, data) async => GomuksHandlePush(handle, data), (handle, data) async => GomuksHandlePush(handle, data),
); );
return response == null ? null : .fromJson(response); return (
Event.fromJson(response["event"]),
RoomMetadata.fromJson(response["room"]),
);
} }
dynamic _sendCommand( dynamic _sendCommand(
@ -270,11 +178,6 @@ class ClientController extends AsyncNotifier<int> {
await _sendCommand("leave_room", {"room_id": room.metadata!.id}); await _sendCommand("leave_room", {"room_id": room.metadata!.id});
} }
// (await _sendCommand("get_event_context", {
// "room_id": request.roomId,
// "event_id": r"$OqZT4NuTj0J1-771IOEEWRI4XdumRNu6ighlvO3K3gc",
// }));
Future<IList<Event>> getRoomState(GetRoomStateRequest request) async { Future<IList<Event>> getRoomState(GetRoomStateRequest request) async {
Future<List?> getState(GetRoomStateRequest request) async => Future<List?> getState(GetRoomStateRequest request) async =>
(await _sendCommand("get_room_state", request.toJson())) as List?; (await _sendCommand("get_room_state", request.toJson())) as List?;

View file

@ -0,0 +1,124 @@
import "dart:async";
import "dart:ffi";
import "package:flutter/foundation.dart";
import "package:nexus/controllers/account_data.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/init_complete.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/controllers/space_edges.dart";
import "package:nexus/controllers/sync_status.dart";
import "package:nexus/controllers/top_level_spaces.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/main.dart";
import "package:ffi/ffi.dart";
import "package:nexus/models/event.dart";
import "package:nexus/models/sync_data.dart";
import "package:nexus/src/third_party/gomuks.g.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
class GomuksListenerController extends AsyncNotifier<void> {
@override
Future<void> build() async {
debugPrint("Starting gomuks...");
final handle = await ref.watch(ClientController.provider.future);
final callable =
NativeCallable<
Void Function(Pointer<Char>, Int64, GomuksOwnedBuffer)
>.listener((
Pointer<Char> command,
int requestId,
GomuksOwnedBuffer data,
) {
try {
final muksEventType = command.cast<Utf8>().toDartString();
debugPrint("Handling $muksEventType...");
final decodedMuksEvent = data.toJson();
switch (muksEventType) {
case "client_state":
ref
.watch(ClientStateController.provider.notifier)
.set(.fromJson(decodedMuksEvent));
break;
case "sync_status":
ref
.watch(SyncStatusController.provider.notifier)
.set(.fromJson(decodedMuksEvent));
break;
case "init_complete":
ref.watch(InitCompleteController.provider.notifier).complete();
break;
case "send_complete":
final event = Event.fromJson(decodedMuksEvent["event"]);
ref
.watch(RoomsController.provider.notifier)
.update(
.new({
event.roomId: .new(events: .new({event.rowId: event})),
}),
.new(),
);
break;
case "sync_complete":
final syncData = SyncData.fromJson(decodedMuksEvent);
final roomProvider = RoomsController.provider;
final accountDataProvider = AccountDataController.provider;
if (syncData.clearState) {
ref.invalidate(roomProvider);
ref.invalidate(accountDataProvider);
}
ref
.watch(roomProvider.notifier)
.update(syncData.rooms, syncData.leftRooms);
ref
.watch(accountDataProvider.notifier)
.update(syncData.accountData);
if (syncData.topLevelSpaces != null) {
ref
.watch(TopLevelSpacesController.provider.notifier)
.set(syncData.topLevelSpaces!);
}
if (syncData.spaceEdges != null) {
ref
.watch(SpaceEdgesController.provider.notifier)
.set(syncData.spaceEdges!);
}
// ref
// .watch(SyncStatusController.provider.notifier)
// .set(SyncStatus.fromJson(decodedMuksEvent));
break;
default:
debugPrint("Unhandled event: $muksEventType");
}
debugPrint("Finished handling $muksEventType...");
} catch (error, stackTrace) {
if (kDebugMode) {
debugPrintStack(stackTrace: stackTrace, label: error.toString());
rethrow;
} else {
showError(error, stackTrace);
}
}
});
ref.onDispose(callable.close);
final errorCode = GomuksStart(handle, callable.nativeFunction);
if (errorCode != 0) {
throw Exception("GomuksStart returned error code $errorCode");
}
}
static final provider = AsyncNotifierProvider<GomuksListenerController, void>(
GomuksListenerController.new,
);
}

View file

@ -8,7 +8,6 @@ import "package:intl/intl.dart";
import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/key.dart";
import "package:nexus/controllers/notification.dart"; import "package:nexus/controllers/notification.dart";
import "package:nexus/controllers/push_key.dart"; import "package:nexus/controllers/push_key.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/main.dart"; import "package:nexus/main.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/client_state.dart";
@ -63,12 +62,11 @@ class UnifiedPushController extends AsyncNotifier<bool> {
"Failed to decrypt notification. Try toggling off and on UnifiedPush in settings.", "Failed to decrypt notification. Try toggling off and on UnifiedPush in settings.",
); );
} }
final event = await ref final (event, roomMetadata) = await ref
.read(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.handlePush(json.decode(String.fromCharCodes(message.content))); .handlePush(json.decode(String.fromCharCodes(message.content)));
if (event == null || if (event.unreadType?.shouldNotify() != true ||
event.unreadType?.shouldNotify() != true ||
(!isInBackground && (!isInBackground &&
await windowManager.isFocused().onError((_, _) => true) && await windowManager.isFocused().onError((_, _) => true) &&
await ref.read( await ref.read(
@ -79,41 +77,19 @@ class UnifiedPushController extends AsyncNotifier<bool> {
return; return;
} }
final provider = RoomsController.provider.select( final icon = roomMetadata.avatar == null
(rooms) => rooms[event.roomId],
);
if (ref.read(provider)?.metadata == null) {
final completer = Completer<void>();
final subscription = ref.listen(provider, (previous, next) {
if (next?.metadata != null && !completer.isCompleted) {
completer.complete();
}
}, fireImmediately: true);
try {
await completer.future.timeout(.new(seconds: 10));
} on TimeoutException {
// metadata didn't show up in time
} finally {
subscription.close();
}
}
final room = ref.read(provider);
final avatar = room?.metadata?.avatar;
final icon = avatar == null
? null ? null
: await ref : await ref
.read(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.downloadMedia(.new(mxc: avatar, isAvatar: true)); .downloadMedia(
.new(mxc: roomMetadata.avatar!, isAvatar: true),
);
await ref await ref
.read(NotificationController.provider.notifier) .read(NotificationController.provider.notifier)
.send( .send(
id: event.eventId.hashCode & 0x7fffffff, id: event.eventId.hashCode & 0x7fffffff,
title: room?.metadata?.name ?? "New Event", title: roomMetadata.name ?? "New Event",
icon: icon, icon: icon,
payload: event.eventId, payload: event.eventId,
body: switch (event.content) { body: switch (event.content) {

View file

@ -4,8 +4,8 @@ import "package:dynamic_color/dynamic_color.dart";
import "package:flutter/foundation.dart"; import "package:flutter/foundation.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:media_kit/media_kit.dart"; import "package:media_kit/media_kit.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart"; import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/gomuks_listener.dart";
import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/key.dart";
import "package:nexus/controllers/member_list_opened.dart"; import "package:nexus/controllers/member_list_opened.dart";
import "package:nexus/controllers/multi_provider.dart"; import "package:nexus/controllers/multi_provider.dart";
@ -152,7 +152,7 @@ class const App({super.key}) extends StatelessWidget {
builder: (_, ref, _) => switch (ref.watch( builder: (_, ref, _) => switch (ref.watch(
MultiProviderController.provider( MultiProviderController.provider(
.new([ .new([
ClientController.provider, GomuksListenerController.provider,
NotificationController.provider, NotificationController.provider,
UnifiedPushController.provider, UnifiedPushController.provider,
MemberListOpenedController.provider, MemberListOpenedController.provider,

View file

@ -17,7 +17,7 @@ buildGoModule (
src = "${src}/gomuks"; src = "${src}/gomuks";
vendorHash = "sha256-e3+bUe7IURUUaLy905a3xlRgKbkHpVfMuGlZCNSxOyU="; vendorHash = "sha256-rLLDbNrYf5HqG3Y6zihRi1v+GqGeNAtZUL+8QHdCz7w=";
buildPhase = '' buildPhase = ''
runHook preBuild runHook preBuild