diff --git a/lib/controllers/client_state_controller.dart b/lib/controllers/client_state_controller.dart index 0e9a505..998d4a1 100644 --- a/lib/controllers/client_state_controller.dart +++ b/lib/controllers/client_state_controller.dart @@ -9,14 +9,6 @@ class ClientStateController extends Notifier { state = newState; } - void setHomeServer(String homeserver){ - state = .new(isInitialized: state?.isInitialized ?? false, - isLoggedIn: state?.isLoggedIn ?? false, - isVerified: state?.isVerified ?? false, - userId: state?.userId, - homeserverUrl: homeserver); - } - static final provider = NotifierProvider( ClientStateController.new, ); diff --git a/lib/main.dart b/lib/main.dart index 377582c..cb1e53d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,7 +11,6 @@ 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/select_server_page.dart"; import "package:nexus/pages/verify_page.dart"; import "package:nexus/widgets/error_dialog.dart"; @@ -124,10 +123,8 @@ class App extends StatelessWidget { return Loading(); } - if ((!clientState.isLoggedIn) && (clientState.homeserverUrl == null || clientState.homeserverUrl?.isEmpty == true)) { + if (!clientState.isLoggedIn) { return SelectServerPage(); - } else if(!clientState.isLoggedIn) { - return LoginPage(); } else if (!clientState.isVerified) { return VerifyPage(); } else { diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 8f8c6a4..5d12d00 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -2,32 +2,81 @@ 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/controllers/client_state_controller.dart"; import "package:nexus/models/requests/login_request.dart"; import "package:nexus/widgets/appbar.dart"; class LoginPage extends HookConsumerWidget { - const LoginPage({super.key}); + final Uri homeserver; + + const LoginPage({super.key, required this.homeserver}); @override Widget build(BuildContext context, WidgetRef ref) { final client = ref.watch(ClientController.provider.notifier); final isLoggingIn = useState(false); - final homeserverUrl = ref.watch(ClientStateController.provider)?.homeserverUrl; + final hasError = useState(false); + final userNameFocusNode = useFocusNode(); + final passwordFocusNode = useFocusNode(); - if(homeserverUrl == null || homeserverUrl.trim().isEmpty) { - throw Exception("Homeserver URL must be set for login."); - } + //This is the safe way to request things directly after page load. + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + userNameFocusNode.requestFocus(); + }); + return null; + }, []); final theme = Theme.of(context); final username = useTextEditingController(); final password = useTextEditingController(); + Future tryLogin() async { + if (isLoggingIn.value) return; + isLoggingIn.value = true; + hasError.value = false; + + final error = await client.login( + LoginRequest( + username: username.text, + password: password.text, + homeserverUrl: homeserver.origin + ) + ); + + if (!context.mounted) return; + + if (error != null) { + hasError.value = true; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Login failed. Is your password right?\nError: $error", + style: TextStyle( + color: theme.colorScheme.onErrorContainer, + ), + ), + backgroundColor: theme.colorScheme.errorContainer, + ), + ); + isLoggingIn.value = false; + } + else{ + Navigator.pop(context); + } + passwordFocusNode.requestFocus(); + isLoggingIn.value = false; + } + return Scaffold( - appBar: Appbar(), + appBar: Appbar( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context) + ), + ), body: AlertDialog( - title: Text("Login"), + title: Text("Login to ${homeserver.host}"), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -37,12 +86,56 @@ class LoginPage extends HookConsumerWidget { ), SizedBox(height: 12), TextField( - decoration: InputDecoration(label: Text("Username")), + focusNode: userNameFocusNode, + textInputAction: TextInputAction.next, + onChanged: (newVal) { + if (hasError.value) { + hasError.value = false; + } + }, + decoration: InputDecoration( + label: Text("Username"), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + width: hasError.value ? 4 : 2, + color: hasError.value + ? theme.colorScheme.error + : theme.colorScheme.primary + ), + ) + ), controller: username, ), SizedBox(height: 12), TextField( - decoration: InputDecoration(label: Text("Password")), + focusNode: passwordFocusNode, + textInputAction: TextInputAction.done, + onSubmitted: (_) => tryLogin(), + onChanged: (newVal) { + if (hasError.value) { + hasError.value = false; + } + }, + selectAllOnFocus: true, + decoration: InputDecoration( + label: Text("Password"), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + width: hasError.value ? 4 : 2, + color: hasError.value + ? theme.colorScheme.error + : theme.colorScheme.primary + ) + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + width: hasError.value ? 4 : 2, + color: hasError.value + ? theme.colorScheme.error + : theme.colorScheme.primary + ), + ) + ), controller: password, obscureText: true, ), @@ -50,33 +143,7 @@ class LoginPage extends HookConsumerWidget { ), actions: [ TextButton( - onPressed: isLoggingIn.value - ? null - : () async { - isLoggingIn.value = true; - final error = await client.login( - LoginRequest( - username: username.text, - password: password.text, - homeserverUrl: homeserverUrl, - ), - ); - - if (error != null && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Login failed. Is your password right?\nError: $error", - style: TextStyle( - color: theme.colorScheme.onErrorContainer, - ), - ), - backgroundColor: theme.colorScheme.errorContainer, - ), - ); - isLoggingIn.value = false; - } - }, + onPressed: () => tryLogin(), child: Text("Sign In"), ), ], diff --git a/lib/pages/select_server_page.dart b/lib/pages/select_server_page.dart index e38d595..af9be91 100644 --- a/lib/pages/select_server_page.dart +++ b/lib/pages/select_server_page.dart @@ -3,9 +3,9 @@ import "package:flutter_hooks/flutter_hooks.dart"; import "package:flutter_svg/flutter_svg.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/client_controller.dart"; -import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/helpers/launch_helper.dart"; import "package:nexus/models/homeserver.dart"; +import "package:nexus/pages/login_page.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/divider_text.dart"; import "package:nexus/widgets/loading.dart"; @@ -17,11 +17,11 @@ class SelectServerPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final client = ref.watch(ClientController.provider.notifier); - + final hasError = useState(false); final isLoading = useState(false); - + final homeserverFocusNode = useFocusNode(); + final launch = ref.watch(LaunchHelper.provider).launchUrl; - final homeserverUrl = useTextEditingController(); Future setHomeserver(Uri? newHomeserver) async { @@ -37,6 +37,7 @@ class SelectServerPage extends HookConsumerWidget { if (context.mounted) { if (newUrl == null) { + hasError.value = true; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -48,15 +49,19 @@ class SelectServerPage extends HookConsumerWidget { ); } else { homeserverUrl.text = newHomeserver!.origin; - ref.watch(ClientStateController.provider.notifier).setHomeServer(newUrl); + Navigator.push(context, MaterialPageRoute(builder: (_) => LoginPage(homeserver: Uri.parse(newUrl)))); } } + + homeserverFocusNode.requestFocus(); isLoading.value = false; } return Scaffold( appBar: Appbar(), - body: Center( + body: isLoading.value + ? const Loading() + : Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: 600), child: Column( @@ -90,14 +95,33 @@ class SelectServerPage extends HookConsumerWidget { children: [ Expanded( child: TextField( + focusNode: homeserverFocusNode, + textInputAction: TextInputAction.done, + onSubmitted: (_) => setHomeserver(Uri.tryParse(homeserverUrl.text)), + onChanged: (newVal) { + if (hasError.value) { + hasError.value = false; + } + }, controller: homeserverUrl, decoration: InputDecoration( labelText: "Homeserver URL (e.g. matrix.org)", hintText: "e.g. matrix.org", + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + width: hasError.value ? 4 : 2, + color: hasError.value + ? theme.colorScheme.error + : theme.colorScheme.primary + ) + ), enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: (homeserverUrl.text.trim().isEmpty) + borderSide: BorderSide( + width: hasError.value ? 4 : 2, + color: hasError.value ? theme.colorScheme.error - : theme.colorScheme.primary) + : theme.colorScheme.primary + ) ), ), ), @@ -174,10 +198,6 @@ class SelectServerPage extends HookConsumerWidget { onPressed: () => launch(Uri.https("servers.joinmatrix.org")), child: Text("See more homeservers..."), ), - if (isLoading.value) - Padding(padding: EdgeInsets.only(top: 32), child: Loading()) - else - Padding(padding: EdgeInsets.only(top: 12)) ], ), ),