working login

This commit is contained in:
Henry Hiles 2026-01-25 14:32:20 +00:00
commit 92bf5b53bf
No known key found for this signature in database
7 changed files with 162 additions and 32 deletions

View file

@ -2,6 +2,7 @@ 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/sync_status_controller.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";
@ -31,18 +32,22 @@ class ClientController extends AsyncNotifier<int> {
switch (muksEventType) { switch (muksEventType) {
case "client_state": case "client_state":
final clientState = ClientState.fromJson(decodedMuksEvent); ref
debugPrint("Received event: $clientState"); .watch(ClientStateController.provider.notifier)
.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":
ref
.watch(SyncStatusController.provider.notifier)
.set(SyncStatus.fromJson(decodedMuksEvent));
break;
default: default:
debugPrint( debugPrint("Unhandled event: $muksEventType");
"Unhandled event: $muksEventType: $decodedMuksEvent",
);
} }
} catch (error, stackTrace) { } catch (error, stackTrace) {
debugPrintStack(stackTrace: stackTrace, label: error.toString()); debugPrintStack(stackTrace: stackTrace, label: error.toString());
@ -73,6 +78,15 @@ class ClientController extends AsyncNotifier<int> {
return response.buf.toJson(); return response.buf.toJson();
} }
Future<bool> verify(String recoveryKey) async {
try {
await sendCommand("verify", {"recovery_key": recoveryKey});
return true;
} catch (_) {
return false;
}
}
Future<bool> login(Login login) async { Future<bool> login(Login login) async {
try { try {
await sendCommand("login", login.toJson()); await sendCommand("login", login.toJson());

View file

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

View file

@ -0,0 +1,20 @@
import "dart:async";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
class MultiProviderController extends AsyncNotifier<void> {
MultiProviderController(this.providers);
final IList<AsyncNotifierProvider> providers;
@override
FutureOr<void> build() async => await Future.wait(
providers.map((provider) => ref.watch(provider.future)),
);
static final provider =
AsyncNotifierProvider.family<
MultiProviderController,
void,
IList<AsyncNotifierProvider>
>(MultiProviderController.new);
}

View file

@ -1,14 +1,18 @@
import "dart:io"; import "dart:io";
import "package:fast_immutable_collections/fast_immutable_collections.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:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_controller.dart";
import "package:nexus/controllers/client_state_controller.dart";
import "package:nexus/controllers/multi_provider_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/chat_page.dart";
import "package:nexus/pages/login_page.dart"; import "package:nexus/pages/login_page.dart";
import "package:nexus/pages/verify_page.dart";
import "package:nexus/widgets/error_dialog.dart"; import "package:nexus/widgets/error_dialog.dart";
import "package:nexus/widgets/loading.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";
import "package:dynamic_system_colors/dynamic_system_colors.dart"; import "package:dynamic_system_colors/dynamic_system_colors.dart";
@ -78,11 +82,11 @@ void main() async {
); );
} }
class App extends ConsumerWidget { class App extends StatelessWidget {
const App({super.key}); const App({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) => DynamicColorBuilder( Widget build(BuildContext context) => DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => MaterialApp( builder: (lightDynamic, darkDynamic) => MaterialApp(
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
@ -96,10 +100,38 @@ class App extends ConsumerWidget {
brightness: Brightness.dark, brightness: Brightness.dark,
)) ))
.theme, .theme,
home: Builder( home: Scaffold(
builder: (context) => ref body: Consumer(
.watch(SharedPrefsController.provider) builder: (_, ref, _) => ref
.betterWhen(data: (_) => LoginPage()), .watch(
MultiProviderController.provider(
IListConst([
SharedPrefsController.provider,
ClientController.provider,
]),
),
)
.betterWhen(
data: (_) => Consumer(
builder: (_, ref, _) {
final clientState = ref.watch(
ClientStateController.provider,
);
if (clientState == null || !clientState.isInitialized) {
return Loading();
}
if (!clientState.isLoggedIn) {
return LoginPage();
} else if (!clientState.isVerified) {
return VerifyPage();
} else {
return ChatPage();
}
},
),
),
),
), ),
), ),
); );

View file

@ -1,6 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:nexus/widgets/chat_page/room_chat.dart"; // import "package:nexus/widgets/chat_page/room_chat.dart";
import "package:nexus/widgets/chat_page/sidebar.dart"; // import "package:nexus/widgets/chat_page/sidebar.dart";
class ChatPage extends StatelessWidget { class ChatPage extends StatelessWidget {
const ChatPage({super.key}); const ChatPage({super.key});
@ -10,23 +10,23 @@ class ChatPage extends StatelessWidget {
builder: (context, constraints) { builder: (context, constraints) {
final isDesktop = constraints.maxWidth > 650; final isDesktop = constraints.maxWidth > 650;
final showMembersByDefault = constraints.maxWidth > 1000; final showMembersByDefault = constraints.maxWidth > 1000;
return Placeholder();
return Scaffold( // return Scaffold(
body: Builder( // body: Builder(
builder: (context) => Row( // builder: (context) => Row(
children: [ // children: [
if (isDesktop) Sidebar(), // if (isDesktop) Sidebar(),
Expanded( // Expanded(
child: RoomChat( // child: RoomChat(
isDesktop: isDesktop, // isDesktop: isDesktop,
showMembersByDefault: showMembersByDefault, // showMembersByDefault: showMembersByDefault,
), // ),
), // ),
], // ],
), // ),
), // ),
drawer: isDesktop ? null : Sidebar(), // drawer: isDesktop ? null : Sidebar(),
); // );
}, },
); );
} }

View file

@ -0,0 +1,46 @@
import "package:flutter/material.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/client_controller.dart";
import "package:nexus/widgets/form_text_input.dart";
class VerifyPage extends HookConsumerWidget {
const VerifyPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final passphraseController = useTextEditingController();
return AlertDialog(
title: Text("Verify"),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Enter your recovery key or passphrase below to unlock encrypted messages.\nYour passphrase is usually not the same as your password.",
),
SizedBox(height: 12),
FormTextInput(
required: false,
autofocus: true,
capitalize: true,
controller: passphraseController,
obscure: true,
title: "Recovery Key or Passphrase",
),
],
),
actions: [
TextButton(
onPressed: () async {
ref
.watch(ClientController.provider.notifier)
.verify(passphraseController.text);
Navigator.of(context).pop();
},
child: Text("Verify"),
),
],
);
}
}

View file

@ -20,11 +20,13 @@ class FormTextInput extends StatelessWidget {
final Widget? trailing; final Widget? trailing;
final InputBorder? border; final InputBorder? border;
final List<TextInputFormatter>? formatters; final List<TextInputFormatter>? formatters;
final bool autofocus;
const FormTextInput({ const FormTextInput({
super.key, super.key,
this.border, this.border,
this.controller, this.controller,
this.autofocus = false,
this.title, this.title,
this.obscure = false, this.obscure = false,
this.readOnly = false, this.readOnly = false,
@ -45,6 +47,7 @@ class FormTextInput extends StatelessWidget {
@override @override
Widget build(BuildContext context) => TextFormField( Widget build(BuildContext context) => TextFormField(
autofocus: autofocus,
controller: controller, controller: controller,
keyboardType: keyboardType, keyboardType: keyboardType,
readOnly: readOnly, readOnly: readOnly,