This commit is contained in:
Henry Hiles 2026-01-23 13:47:21 +00:00
commit 2a86bdafeb
No known key found for this signature in database
12 changed files with 156 additions and 199 deletions

View file

@ -1,37 +1,36 @@
import "dart:convert";
import "dart:ffi";
import "dart:io";
import "package:ffi/ffi.dart";
import "package:flutter/foundation.dart";
import "package:matrix/encryption.dart";
import "package:nexus/controllers/database_controller.dart";
import "package:nexus/helpers/extensions/gomuks_buffer.dart";
import "package:nexus/models/client_state.dart";
import "package:nexus/models/sync_status.dart";
import "package:nexus/src/third_party/gomuks.g.dart";
import "package:matrix/matrix.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/secure_storage_controller.dart";
import "package:nexus/models/session_backup.dart";
void gomuksCallback(Pointer<Char> command, int requestId, GomuksBuffer data) {
// Convert the C string to Dart
final cmdStr = command.cast<Utf8>().toDartString();
print("Received event: $cmdStr (requestId=$requestId)");
try {
final muksEventType = command.cast<Utf8>().toDartString();
final Map<String, dynamic> decodedMuksEvent = data.toJson();
// Optionally inspect 'data' if you need
switch (muksEventType) {
case "client_state":
final clientState = ClientState.fromJson(decodedMuksEvent);
debugPrint("Received event: $clientState");
case "sync_status":
final syncStatus = SyncStatus.fromJson(decodedMuksEvent);
debugPrint("Received event: $syncStatus");
}
} catch (error, stackTrace) {
debugPrintStack(stackTrace: stackTrace, label: error.toString());
}
}
class ClientController extends AsyncNotifier<Client> {
class ClientController extends Notifier<int> {
@override
bool updateShouldNotify(
AsyncValue<Client> previous,
AsyncValue<Client> next,
) =>
previous.hasValue != next.hasValue ||
previous.value?.accessToken != next.value?.accessToken;
static const sessionBackupKey = "sessionBackup";
@override
Future<Client> build() async {
int build() {
final handle = GomuksInit();
ref.onDispose(() => GomuksDestroy(handle));
GomuksStart(
handle,
@ -40,80 +39,29 @@ class ClientController extends AsyncNotifier<Client> {
),
);
final client = Client(
"nexus",
logLevel: kReleaseMode ? Level.warning : Level.verbose,
importantStateEvents: {"im.ponies.room_emotes"},
supportedLoginTypes: {AuthenticationTypes.password},
verificationMethods: {KeyVerificationMethod.emoji},
database: await MatrixSdkDatabase.init(
"nexus",
database: await ref.watch(DatabaseController.provider.future),
),
);
final backupJson = await ref
.watch(SecureStorageController.provider.notifier)
.get(sessionBackupKey);
if (backupJson != null) {
final backup = SessionBackup.fromJson(json.decode(backupJson));
await client.init(
waitForFirstSync: false,
newToken: backup.accessToken,
newHomeserver: backup.homeserver,
newUserID: backup.userID,
newDeviceID: backup.deviceID,
newDeviceName: backup.deviceName,
);
}
return client;
return handle;
}
Future<bool> setHomeserver(Uri homeserverUrl) async {
final client = await future;
(int requestId, Map<String, dynamic> response) sendCommand(
String command,
Map<String, dynamic> data,
) {
final responsePtr = calloc<GomuksBuffer>();
try {
await client.checkHomeserver(homeserverUrl);
return true;
} catch (_) {
return false;
}
}
Future<bool> login(String username, String password) async {
final client = await future;
try {
final deviceName = "Nexus Client login on ${Platform.localHostname}";
final details = await MatrixApi(homeserver: client.homeserver).login(
LoginType.mLoginPassword,
initialDeviceDisplayName: deviceName,
identifier: AuthenticationUserIdentifier(user: username),
password: password,
final requestId = GomuksSubmitCommand(
state,
command.toNativeUtf8().cast<Char>(),
data.toGomuksBuffer(),
responsePtr,
);
await ref
.watch(SecureStorageController.provider.notifier)
.set(
sessionBackupKey,
json.encode(
SessionBackup(
accessToken: details.accessToken,
homeserver: client.homeserver!,
userID: details.userId,
deviceID: details.deviceId,
deviceName: deviceName,
).toJson(),
),
);
ref.invalidateSelf(asReload: true);
return true;
} catch (_) {
return false;
return (requestId, responsePtr.ref.toJson());
} finally {
calloc.free(responsePtr);
}
}
static final provider = AsyncNotifierProvider<ClientController, Client>(
static final provider = NotifierProvider<ClientController, int>(
ClientController.new,
);
}

View file

@ -0,0 +1,41 @@
import "dart:convert";
import "dart:ffi";
import "dart:typed_data";
import "package:ffi/ffi.dart";
import "package:nexus/src/third_party/gomuks.g.dart";
extension GomuksBufferX on GomuksBuffer {
/// Safely converts the Go buffer into a Dart `Uint8List`
Uint8List toBytes() {
if (base == nullptr || length <= 0) return Uint8List(0);
return base.asTypedList(length);
}
/// Decodes the bytes as JSON
Map<String, dynamic> toJson() {
final bytes = toBytes();
if (bytes.isEmpty) return {};
return jsonDecode(utf8.decode(bytes));
}
}
extension JsonToGomuksBuffer on Map<String, dynamic> {
GomuksBuffer toGomuksBuffer() {
final jsonString = json.encode(this);
final bytes = utf8.encode(jsonString);
final dataPtr = calloc<Uint8>(bytes.length);
dataPtr.asTypedList(bytes.length).setAll(0, bytes);
final bufPtr = calloc<GomuksBuffer>();
bufPtr.ref.base = dataPtr;
bufPtr.ref.length = bytes.length;
final bufByValue = bufPtr.ref;
calloc.free(bufPtr);
return bufByValue;
}
}

View file

@ -1,17 +1,11 @@
import "dart:io";
import "package:flutter/foundation.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/shared_prefs_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/scheme_to_theme.dart";
import "package:nexus/pages/chat_page.dart";
import "package:nexus/pages/login_page.dart";
import "package:nexus/pages/settings_page.dart";
import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/widgets/loading.dart";
import "package:window_manager/window_manager.dart";
import "package:flutter/material.dart";
import "package:dynamic_system_colors/dynamic_system_colors.dart";
@ -103,37 +97,46 @@ class App extends ConsumerWidget {
builder: (context) => ref
.watch(SharedPrefsController.provider)
.betterWhen(
data: (_) => ref
.watch(ClientController.provider)
.betterWhen(
data: (client) =>
client.accessToken == null ? LoginPage() : ChatPage(),
loading: () => Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
Text(
"Syncing...",
style: Theme.of(context).textTheme.headlineMedium,
),
Loading(),
],
),
),
appBar: Appbar(
actions: [
IconButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => SettingsPage()),
),
icon: Icon(Icons.settings),
),
],
),
),
),
data: (_) {
final response = ref
.watch(ClientController.provider.notifier)
.sendCommand("login", {
"homeserver_url": "federated.nexus",
"username": "quadradical",
"password": "Quadmarad1!",
});
debugPrint("$response");
return Placeholder();
},
// .betterWhen(
// data: (client) =>
// client.accessToken == null ? LoginPage() : ChatPage(),
// loading: () => Scaffold(
// body: Center(
// child: Column(
// mainAxisSize: MainAxisSize.min,
// spacing: 16,
// children: [
// Text(
// "Syncing...",
// style: Theme.of(context).textTheme.headlineMedium,
// ),
// Loading(),
// ],
// ),
// ),
// appBar: Appbar(
// actions: [
// IconButton(
// onPressed: () => Navigator.of(context).push(
// MaterialPageRoute(builder: (_) => SettingsPage()),
// ),
// icon: Icon(Icons.settings),
// ),
// ],
// ),
// ),
// ),
),
),
),

View file

@ -0,0 +1,15 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "client_state.freezed.dart";
part "client_state.g.dart";
@freezed
abstract class ClientState with _$ClientState {
const factory ClientState({
required bool isInitialized,
required bool isLoggedIn,
required bool isVerified,
}) = _ClientState;
factory ClientState.fromJson(Map<String, Object?> json) =>
_$ClientStateFromJson(json);
}

View file

@ -1,13 +0,0 @@
import "package:freezed_annotation/freezed_annotation.dart";
import "package:matrix/matrix.dart";
part "full_room.freezed.dart";
@freezed
abstract class FullRoom with _$FullRoom {
const FullRoom._();
const factory FullRoom({
required Room roomData,
required String title,
required Uri? avatar,
}) = _FullRoom;
}

View file

@ -1,12 +0,0 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "homeserver.freezed.dart";
@freezed
abstract class Homeserver with _$Homeserver {
const factory Homeserver({
required String name,
required String description,
required Uri url,
required String iconUrl,
}) = _Homeserver;
}

View file

@ -1,11 +0,0 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "image_data.freezed.dart";
@freezed
abstract class ImageData with _$ImageData {
const factory ImageData({
required String uri,
required int? height,
required int? width,
}) = _ImageData;
}

View file

@ -1 +0,0 @@
enum RelationType { edit, reply }

View file

@ -1,17 +0,0 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "session_backup.freezed.dart";
part "session_backup.g.dart";
@freezed
abstract class SessionBackup with _$SessionBackup {
const factory SessionBackup({
required String accessToken,
required Uri homeserver,
required String userID,
required String deviceID,
required String deviceName,
}) = _SessionBackup;
factory SessionBackup.fromJson(Map<String, Object?> json) =>
_$SessionBackupFromJson(json);
}

View file

@ -1,20 +0,0 @@
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter/widgets.dart";
import "package:freezed_annotation/freezed_annotation.dart";
import "package:matrix/matrix.dart";
import "package:nexus/models/full_room.dart";
part "space.freezed.dart";
@freezed
abstract class Space with _$Space {
const Space._();
const factory Space({
required String title,
required String id,
required IList<FullRoom> children,
required Client client,
Room? roomData,
Uri? avatar,
IconData? icon,
}) = _Space;
}

View file

@ -0,0 +1,18 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "sync_status.freezed.dart";
part "sync_status.g.dart";
@freezed
abstract class SyncStatus with _$SyncStatus {
const factory SyncStatus({
required Type type,
required int errorCount,
required int lastSync,
}) = _SyncStatus;
factory SyncStatus.fromJson(Map<String, Object?> json) =>
_$SyncStatusFromJson(json);
}
@JsonEnum(fieldRename: FieldRename.snake)
enum Type { ok, waiting, erroring, permanentlyFailed }