working login page

This commit is contained in:
Henry Hiles 2026-01-25 13:02:40 +00:00
commit 7c6ddab6a3
No known key found for this signature in database
12 changed files with 133 additions and 245 deletions

View file

@ -1,17 +1,30 @@
import "dart:ffi"; import "dart:ffi";
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/sync_status_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/sync_status.dart"; import "package:nexus/models/sync_status.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";
void gomuksCallback( class ClientController extends AsyncNotifier<int> {
@override
Future<int> build() async {
final handle = await Isolate.run(GomuksInit);
ref.onDispose(() => GomuksDestroy(handle));
GomuksStart(
handle,
NativeCallable<
Void Function(Pointer<Char>, Int64, GomuksBorrowedBuffer)
>.listener((
Pointer<Char> command, Pointer<Char> command,
int requestId, int requestId,
GomuksBorrowedBuffer data, GomuksBorrowedBuffer data,
) { ) {
try { try {
final muksEventType = command.cast<Utf8>().toDartString(); final muksEventType = command.cast<Utf8>().toDartString();
final Map<String, dynamic> decodedMuksEvent = data.toJson(); final Map<String, dynamic> decodedMuksEvent = data.toJson();
@ -22,40 +35,37 @@ void gomuksCallback(
debugPrint("Received event: $clientState"); debugPrint("Received event: $clientState");
break; break;
case "sync_status": case "sync_status":
final syncStatus = SyncStatus.fromJson(decodedMuksEvent); ref
debugPrint("Received event: $syncStatus"); .watch(SyncStatusController.provider.notifier)
.set(SyncStatus.fromJson(decodedMuksEvent));
break; break;
default: default:
debugPrint("Unhandled event: $muksEventType: $decodedMuksEvent"); debugPrint(
"Unhandled event: $muksEventType: $decodedMuksEvent",
);
} }
} catch (error, stackTrace) { } catch (error, stackTrace) {
debugPrintStack(stackTrace: stackTrace, label: error.toString()); debugPrintStack(stackTrace: stackTrace, label: error.toString());
} }
} })
class ClientController extends Notifier<int> {
@override
int build() {
final handle = GomuksInit();
ref.onDispose(() => GomuksDestroy(handle));
GomuksStart(
handle,
NativeCallable<
Void Function(Pointer<Char>, Int64, GomuksBorrowedBuffer)
>.listener(gomuksCallback)
.nativeFunction, .nativeFunction,
); );
return handle; return handle;
} }
Map<String, dynamic> sendCommand(String command, Map<String, dynamic> data) { Future<Map<String, dynamic>> sendCommand(
String command,
Map<String, dynamic> data,
) async {
final bufferPointer = data.toGomuksBufferPtr(); final bufferPointer = data.toGomuksBufferPtr();
final response = GomuksSubmitCommand( final handle = await future;
state, final response = await Isolate.run(
() => GomuksSubmitCommand(
handle,
command.toNativeUtf8().cast<Char>(), command.toNativeUtf8().cast<Char>(),
bufferPointer.ref, bufferPointer.ref,
),
); );
calloc.free(bufferPointer); calloc.free(bufferPointer);
@ -63,7 +73,27 @@ class ClientController extends Notifier<int> {
return response.buf.toJson(); return response.buf.toJson();
} }
static final provider = NotifierProvider<ClientController, int>( Future<bool> login(Login login) async {
try {
await sendCommand("login", login.toJson());
return true;
} catch (_) {
return false;
}
}
Future<String?> discoverHomeserver(Uri homeserver) async {
try {
final response = await sendCommand("discover_homeserver", {
"user_id": "@fakeuser:${homeserver.host}",
});
return (response["m.homeserver"] as Map<String, dynamic>)["base_url"];
} catch (_) {
return null;
}
}
static final provider = AsyncNotifierProvider<ClientController, int>(
ClientController.new, ClientController.new,
); );
} }

View file

@ -0,0 +1,13 @@
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/models/sync_status.dart";
class SyncStatusController extends Notifier<SyncStatus?> {
@override
Null build() => null;
void set(SyncStatus newStatus) => state = newStatus;
static final provider = NotifierProvider<SyncStatusController, SyncStatus?>(
SyncStatusController.new,
);
}

View file

@ -3,8 +3,11 @@ import "package:flutter/foundation.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/controllers/shared_prefs_controller.dart"; import "package:nexus/controllers/shared_prefs_controller.dart";
import "package:nexus/controllers/sync_status_controller.dart";
import "package:nexus/helpers/extensions/better_when.dart"; import "package:nexus/helpers/extensions/better_when.dart";
import "package:nexus/helpers/extensions/scheme_to_theme.dart"; import "package:nexus/helpers/extensions/scheme_to_theme.dart";
import "package:nexus/models/sync_status.dart";
import "package:nexus/pages/login_page.dart";
import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/error_dialog.dart";
import "package:window_manager/window_manager.dart"; import "package:window_manager/window_manager.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
@ -96,50 +99,7 @@ class App extends ConsumerWidget {
home: Builder( home: Builder(
builder: (context) => ref builder: (context) => ref
.watch(SharedPrefsController.provider) .watch(SharedPrefsController.provider)
.betterWhen( .betterWhen(data: (_) => LoginPage()),
data: (_) => ElevatedButton(
onPressed: () async {
final response = ref
.watch(ClientController.provider.notifier)
.sendCommand("login", {
"homeserver_url": "https://matrix.federated.nexus",
"username": "quadradical",
"password": "Quadfnrad1!",
});
print(response);
},
child: Text("foo"),
),
// .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,12 @@
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;
}

14
lib/models/login.dart Normal file
View file

@ -0,0 +1,14 @@
import "package:freezed_annotation/freezed_annotation.dart";
part "login.freezed.dart";
part "login.g.dart";
@freezed
abstract class Login with _$Login {
const factory Login({
required String username,
required String password,
required String homeserverUrl,
}) = _Login;
factory Login.fromJson(Map<String, Object?> json) => _$LoginFromJson(json);
}

View file

@ -5,7 +5,7 @@ part "sync_status.g.dart";
@freezed @freezed
abstract class SyncStatus with _$SyncStatus { abstract class SyncStatus with _$SyncStatus {
const factory SyncStatus({ const factory SyncStatus({
required Type type, required SyncStatusType type,
required int errorCount, required int errorCount,
required int lastSync, required int lastSync,
}) = _SyncStatus; }) = _SyncStatus;
@ -15,4 +15,4 @@ abstract class SyncStatus with _$SyncStatus {
} }
@JsonEnum(fieldRename: FieldRename.snake) @JsonEnum(fieldRename: FieldRename.snake)
enum Type { ok, waiting, erroring, permanentlyFailed } enum SyncStatusType { ok, waiting, erroring, permanentlyFailed }

View file

@ -5,6 +5,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/helpers/launch_helper.dart";
import "package:nexus/models/homeserver.dart"; import "package:nexus/models/homeserver.dart";
import "package:nexus/models/login.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/divider_text.dart";
import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/loading.dart";
@ -15,27 +16,25 @@ class LoginPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final client = ref.watch(ClientController.provider.notifier);
final isLoading = useState(false); final isLoading = useState(false);
final allowLogin = useState(false); final homeserver = useState<String?>(null);
final launch = ref.watch(LaunchHelper.provider).launchUrl; final launch = ref.watch(LaunchHelper.provider).launchUrl;
Future<void> setHomeserver(Uri? homeserver) async { Future<void> setHomeserver(Uri? newHomeserver) async {
isLoading.value = true; isLoading.value = true;
final succeeded = homeserver == null
? false homeserver.value = newHomeserver == null
: await ref ? null
.watch(ClientController.provider.notifier) : await client.discoverHomeserver(
.setHomeserver( newHomeserver.hasScheme
homeserver.hasScheme ? newHomeserver
? homeserver : Uri.https(newHomeserver.path),
: Uri.https(homeserver.path),
); );
if (succeeded) { if (homeserver.value == null && context.mounted) {
allowLogin.value = true;
} else if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
@ -157,7 +156,7 @@ class LoginPage extends HookConsumerWidget {
), ),
if (isLoading.value) if (isLoading.value)
Padding(padding: EdgeInsets.only(top: 32), child: Loading()) Padding(padding: EdgeInsets.only(top: 32), child: Loading())
else if (allowLogin.value) ...[ else if (homeserver.value != null) ...[
DividerText("Then, sign in:"), DividerText("Then, sign in:"),
SizedBox(height: 4), SizedBox(height: 4),
TextField( TextField(
@ -174,9 +173,13 @@ class LoginPage extends HookConsumerWidget {
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
isLoading.value = true; isLoading.value = true;
final succeeded = await ref final succeeded = await client.login(
.watch(ClientController.provider.notifier) Login(
.login(username.text, password.text); username: username.text,
password: password.text,
homeserverUrl: homeserver.value!,
),
);
if (!succeeded && context.mounted) { if (!succeeded && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

View file

@ -13,7 +13,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_vodozemac
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

View file

@ -73,14 +73,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
base58check:
dependency: transitive
description:
name: base58check
sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
blurhash_dart: blurhash_dart:
dependency: transitive dependency: transitive
description: description:
@ -105,14 +97,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.3" version: "4.0.3"
build_cli_annotations:
dependency: transitive
description:
name: build_cli_annotations
sha256: e563c2e01de8974566a1998410d3f6f03521788160a02503b0b1f1a46c7b3d95
url: "https://pub.dev"
source: hosted
version: "2.1.1"
build_config: build_config:
dependency: transitive dependency: transitive
description: description:
@ -153,14 +137,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.12.1" version: "8.12.1"
canonical_json:
dependency: transitive
description:
name: canonical_json
sha256: d6be1dd66b420c6ac9f42e3693e09edf4ff6edfee26cb4c28c1c019fdb8c0c15
url: "https://pub.dev"
source: hosted
version: "1.1.2"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -557,14 +533,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.0" version: "3.1.0"
flutter_rust_bridge:
dependency: transitive
description:
name: flutter_rust_bridge
sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -626,14 +594,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_vodozemac:
dependency: "direct main"
description:
name: flutter_vodozemac
sha256: "16d4b44dd338689441fe42a80d0184e5c864e9563823de9e7e6371620d2c0590"
url: "https://pub.dev"
source: hosted
version: "0.4.1"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -768,14 +728,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.15.6" version: "0.15.6"
html_unescape:
dependency: transitive
description:
name: html_unescape
sha256: "15362d7a18f19d7b742ef8dcb811f5fd2a2df98db9f80ea393c075189e0b61e3"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
http: http:
dependency: transitive dependency: transitive
description: description:
@ -960,14 +912,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
markdown:
dependency: transitive
description:
name: markdown
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
url: "https://pub.dev"
source: hosted
version: "7.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -984,14 +928,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.11.1"
matrix:
dependency: "direct main"
description:
name: matrix
sha256: fb116ee89f6871441f22f76a988db15cfcfb6dfac97e3e2d654c240080015707
url: "https://pub.dev"
source: hosted
version: "4.1.0"
mention_tag_text_field: mention_tag_text_field:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1192,14 +1128,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.2" version: "3.2.2"
random_string:
dependency: transitive
description:
name: random_string
sha256: "03b52435aae8cbdd1056cf91bfc5bf845e9706724dd35ae2e99fa14a1ef79d02"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:
@ -1280,14 +1208,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.26.3" version: "1.26.3"
sdp_transform:
dependency: transitive
description:
name: sdp_transform
sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
sembast: sembast:
dependency: transitive dependency: transitive
description: description:
@ -1389,14 +1309,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
slugify:
dependency: transitive
description:
name: slugify
sha256: b272501565cb28050cac2d96b7bf28a2d24c8dae359280361d124f3093d337c3
url: "https://pub.dev"
source: hosted
version: "2.0.0"
source_gen: source_gen:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
@ -1437,30 +1349,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
sqflite_common_ffi:
dependency: "direct main"
description:
name: sqflite_common_ffi
sha256: "8d7b8749a516cbf6e9057f9b480b716ad14fc4f3d3873ca6938919cc626d9025"
url: "https://pub.dev"
source: hosted
version: "2.3.7+1"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
url: "https://pub.dev"
source: hosted
version: "2.9.4"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -1581,14 +1469,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.3.1"
unorm_dart:
dependency: transitive
description:
name: unorm_dart
sha256: "5b35bff83fce4d76467641438f9e867dc9bcfdb8c1694854f230579d68cd8f4b"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1701,14 +1581,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.0.2"
vodozemac:
dependency: "direct main"
description:
name: vodozemac
sha256: "39144e20740807731871c9248d811ed5a037b21d0aa9ffcfa630954de74139d9"
url: "https://pub.dev"
source: hosted
version: "0.4.0"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@ -1749,14 +1621,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.1"
webrtc_interface:
dependency: transitive
description:
name: webrtc_interface
sha256: "2e604a31703ad26781782fb14fa8a4ee621154ee2c513d2b9938e486fa695233"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View file

@ -54,14 +54,10 @@ dependencies:
git: git:
url: https://github.com/Henry-Hiles/flutter_chat_ui url: https://github.com/Henry-Hiles/flutter_chat_ui
path: packages/flutter_link_previewer path: packages/flutter_link_previewer
matrix: ^4.1.0
sqflite_common_ffi: ^2.3.6
color_hash: ^1.0.1 color_hash: ^1.0.1
flutter_vodozemac: ^0.4.1
flutter_widget_from_html_core: ^0.17.0 flutter_widget_from_html_core: ^0.17.0
flutter_svg: ^2.2.2 flutter_svg: ^2.2.2
json_annotation: ^4.9.0 json_annotation: ^4.9.0
vodozemac: ^0.4.0
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
mention_tag_text_field: ^0.0.9 mention_tag_text_field: ^0.0.9
fluttertagger: ^2.3.1 fluttertagger: ^2.3.1

View file

@ -12,8 +12,6 @@ void main(List<String> args) async {
print("Cloning Gomuks repository..."); print("Cloning Gomuks repository...");
final cloneResult = await Process.run("git", [ final cloneResult = await Process.run("git", [
"clone", "clone",
"--branch",
"tulir/ffi",
"--depth", "--depth",
"1", "1",
"https://mau.dev/gomuks/gomuks", "https://mau.dev/gomuks/gomuks",
@ -32,7 +30,7 @@ void main(List<String> args) async {
dartFile: Platform.script.resolve("../lib/src/third_party/gomuks.g.dart"), dartFile: Platform.script.resolve("../lib/src/third_party/gomuks.g.dart"),
), ),
headers: Headers( headers: Headers(
entryPoints: [File(join(repoDir.path, "pkg", "ffi", "ffi.h")).uri], entryPoints: [File(join(repoDir.path, "pkg", "ffi", "gomuksffi.h")).uri],
compilerOptions: ["--no-warnings"], compilerOptions: ["--no-warnings"],
), ),
functions: Functions.includeAll, functions: Functions.includeAll,

View file

@ -13,7 +13,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_vodozemac
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)