From 5ad85d3f6293befb1f923879f6a7ee85fa64b0ba Mon Sep 17 00:00:00 2001 From: istalri Date: Tue, 2 Jun 2026 09:29:52 +0200 Subject: [PATCH 1/7] Login is on seperated page and for the server selection only the cards instead of the entire page are scrollable. --- lib/controllers/client_state_controller.dart | 8 + lib/main.dart | 5 +- lib/pages/login_page.dart | 204 ++++--------------- lib/pages/select_server_page.dart | 187 +++++++++++++++++ pubspec.lock | 16 +- 5 files changed, 247 insertions(+), 173 deletions(-) create mode 100644 lib/pages/select_server_page.dart diff --git a/lib/controllers/client_state_controller.dart b/lib/controllers/client_state_controller.dart index 998d4a1..0e9a505 100644 --- a/lib/controllers/client_state_controller.dart +++ b/lib/controllers/client_state_controller.dart @@ -9,6 +9,14 @@ 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 dcf7b67..377582c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ 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"; import "package:nexus/widgets/loading.dart"; @@ -123,7 +124,9 @@ class App extends StatelessWidget { return Loading(); } - if (!clientState.isLoggedIn) { + if ((!clientState.isLoggedIn) && (clientState.homeserverUrl == null || clientState.homeserverUrl?.isEmpty == true)) { + return SelectServerPage(); + } else if(!clientState.isLoggedIn) { return LoginPage(); } else if (!clientState.isVerified) { return VerifyPage(); diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index d2153eb..8f8c6a4 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -1,185 +1,64 @@ import "package:flutter/material.dart"; 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/helpers/launch_helper.dart"; -import "package:nexus/models/homeserver.dart"; +import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/models/requests/login_request.dart"; import "package:nexus/widgets/appbar.dart"; -import "package:nexus/widgets/divider_text.dart"; -import "package:nexus/widgets/loading.dart"; class LoginPage extends HookConsumerWidget { const LoginPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); final client = ref.watch(ClientController.provider.notifier); + final isLoggingIn = useState(false); + final homeserverUrl = ref.watch(ClientStateController.provider)?.homeserverUrl; - final isLoading = useState(false); - final homeserver = useState(null); - - final launch = ref.watch(LaunchHelper.provider).launchUrl; - - Future setHomeserver(Uri? newHomeserver) async { - isLoading.value = true; - - homeserver.value = newHomeserver == null - ? null - : await client.discoverHomeserver( - newHomeserver.hasScheme - ? newHomeserver - : Uri.https(newHomeserver.path), - ); - - if (homeserver.value == null && context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Homeserver verification failed. Is your homeserver down?", - style: TextStyle(color: theme.colorScheme.onErrorContainer), - ), - backgroundColor: theme.colorScheme.errorContainer, - ), - ); - } - isLoading.value = false; + if(homeserverUrl == null || homeserverUrl.trim().isEmpty) { + throw Exception("Homeserver URL must be set for login."); } - final homeserverUrl = useTextEditingController(); + final theme = Theme.of(context); + final username = useTextEditingController(); final password = useTextEditingController(); return Scaffold( appBar: Appbar(), - body: Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 600), - child: ListView( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 64), - children: [ - Row( - children: [ - SvgPicture.asset("assets/icon.svg", width: 128), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Nexus", style: theme.textTheme.displayMedium), - Text( - "A Simple Matrix Client", - style: theme.textTheme.headlineMedium, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - Padding( - padding: EdgeInsetsGeometry.symmetric(vertical: 12), - child: Divider(), - ), - - DividerText("Enter a homeserver domain:"), - Row( - spacing: 8, - children: [ - Expanded( - child: TextField( - controller: homeserverUrl, - decoration: InputDecoration( - labelText: "Homeserver URL (e.g. matrix.org)", - ), - ), - ), - IconButton.filled( - tooltip: "Confirm homeserver choice", - onPressed: isLoading.value - ? null - : () => setHomeserver(Uri.tryParse(homeserverUrl.text)), - icon: Icon(Icons.check), - ), - ], - ), - - DividerText("Or, choose from some popular homeservers:"), - ...([ - Homeserver( - name: "Matrix.org", - description: - "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", - url: Uri.https("matrix.org"), - iconUrl: - "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", - ), - Homeserver( - name: "Federated Nexus", - description: - "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", - url: Uri.https("federated.nexus"), - iconUrl: "https://federated.nexus/images/icon.png", - ), - Homeserver( - name: "Unredacted", - description: - "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.", - url: Uri.https("unredacted.org", "services/si/matrix"), - iconUrl: "https://unredacted.org/favicon.ico", - ), - ].map( - (homeserver) => Card( - child: ListTile( - title: Text(homeserver.name), - leading: Image.network( - homeserver.iconUrl, - errorBuilder: (_, _, _) => SizedBox.shrink(), - height: 32, - ), - subtitle: Text(homeserver.description), - onTap: isLoading.value - ? null - : () => setHomeserver(homeserver.url), - trailing: IconButton( - tooltip: "Launch homeserver info page", - onPressed: () => launch(homeserver.url), - icon: Icon(Icons.info_outline), - ), - ), - ), - )), - SizedBox(height: 8), - TextButton( - onPressed: () => launch(Uri.https("servers.joinmatrix.org")), - child: Text("See more homeservers..."), - ), - if (isLoading.value) - Padding(padding: EdgeInsets.only(top: 32), child: Loading()) - else if (homeserver.value != null) ...[ - DividerText("Then, sign in:"), - SizedBox(height: 4), - TextField( - decoration: InputDecoration(label: Text("Username")), - controller: username, - ), - SizedBox(height: 12), - TextField( - decoration: InputDecoration(label: Text("Password")), - controller: password, - obscureText: true, - ), - SizedBox(height: 12), - ElevatedButton( - onPressed: () async { - isLoading.value = true; + body: AlertDialog( + title: Text("Login"), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enter your login credentials:", + ), + SizedBox(height: 12), + TextField( + decoration: InputDecoration(label: Text("Username")), + controller: username, + ), + SizedBox(height: 12), + TextField( + decoration: InputDecoration(label: Text("Password")), + controller: password, + obscureText: true, + ), + ], + ), + actions: [ + TextButton( + onPressed: isLoggingIn.value + ? null + : () async { + isLoggingIn.value = true; final error = await client.login( LoginRequest( username: username.text, password: password.text, - homeserverUrl: homeserver.value!, + homeserverUrl: homeserverUrl, ), ); @@ -195,15 +74,12 @@ class LoginPage extends HookConsumerWidget { backgroundColor: theme.colorScheme.errorContainer, ), ); - isLoading.value = false; + isLoggingIn.value = false; } - }, - child: Text("Sign In"), - ), - ], - ], + }, + child: Text("Sign In"), ), - ), + ], ), ); } diff --git a/lib/pages/select_server_page.dart b/lib/pages/select_server_page.dart new file mode 100644 index 0000000..e38d595 --- /dev/null +++ b/lib/pages/select_server_page.dart @@ -0,0 +1,187 @@ +import "package:flutter/material.dart"; +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/widgets/appbar.dart"; +import "package:nexus/widgets/divider_text.dart"; +import "package:nexus/widgets/loading.dart"; + +class SelectServerPage extends HookConsumerWidget { + const SelectServerPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final client = ref.watch(ClientController.provider.notifier); + + final isLoading = useState(false); + + final launch = ref.watch(LaunchHelper.provider).launchUrl; + + final homeserverUrl = useTextEditingController(); + + Future setHomeserver(Uri? newHomeserver) async { + isLoading.value = true; + + if(newHomeserver?.hasScheme == false){ + newHomeserver = Uri.https(newHomeserver!.path); + } + + final newUrl = newHomeserver == null + ? null + : await client.discoverHomeserver(newHomeserver); + + if (context.mounted) { + if (newUrl == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Homeserver verification failed. Is your homeserver down?", + style: TextStyle(color: theme.colorScheme.onErrorContainer), + ), + backgroundColor: theme.colorScheme.errorContainer, + ), + ); + } else { + homeserverUrl.text = newHomeserver!.origin; + ref.watch(ClientStateController.provider.notifier).setHomeServer(newUrl); + } + } + isLoading.value = false; + } + + return Scaffold( + appBar: Appbar(), + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 600), + child: Column( + children: [ + Row( + children: [ + SvgPicture.asset("assets/icon.svg", width: 128), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Nexus", style: theme.textTheme.displayMedium), + Text( + "A Simple Matrix Client", + style: theme.textTheme.headlineMedium, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + Padding( + padding: EdgeInsetsGeometry.symmetric(vertical: 12), + child: Divider(), + ), + DividerText("Enter a homeserver domain:"), + Row( + spacing: 8, + children: [ + Expanded( + child: TextField( + controller: homeserverUrl, + decoration: InputDecoration( + labelText: "Homeserver URL (e.g. matrix.org)", + hintText: "e.g. matrix.org", + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: (homeserverUrl.text.trim().isEmpty) + ? theme.colorScheme.error + : theme.colorScheme.primary) + ), + ), + ), + ), + IconButton.filled( + tooltip: "Confirm homeserver choice", + onPressed: isLoading.value + ? null + : () => setHomeserver(Uri.tryParse(homeserverUrl.text)), + icon: Icon(Icons.check), + ), + ], + ), + Expanded( + child: ListView( + padding: EdgeInsets.only(top: 12), + children: [ + DividerText("Or, choose from some popular homeservers:"), + ...([ + Homeserver( + name: "Matrix.org", + description: + "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", + url: Uri.https("matrix.org"), + iconUrl: + "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", + ), + Homeserver( + name: "Federated Nexus", + description: + "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", + url: Uri.https("federated.nexus"), + iconUrl: "https://federated.nexus/images/icon.png", + ), + Homeserver( + name: "Unredacted", + description: + "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.", + url: Uri.https("unredacted.org", "services/si/matrix"), + iconUrl: "https://unredacted.org/favicon.ico", + ), + Homeserver( + name: "Lorem ipsum", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + url: Uri.https("loremipsum.io"), + iconUrl: "https://loremipsum.io/favicon.ico" + ), + ].map( + (homeserver) => Card( + child: ListTile( + title: Text(homeserver.name), + leading: Image.network( + homeserver.iconUrl, + errorBuilder: (_, _, _) => SizedBox.shrink(), + height: 32, + ), + subtitle: Text(homeserver.description), + onTap: isLoading.value + ? null + : () => setHomeserver(homeserver.url), + trailing: IconButton( + tooltip: "Launch homeserver info page", + onPressed: () => launch(homeserver.url), + icon: Icon(Icons.info_outline), + ), + ), + ), + )), + ], + ), + ), + SizedBox(height: 5), + TextButton( + 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)) + ], + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 108474b..a7aa53e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -836,10 +836,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -1321,26 +1321,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.17" timeago: dependency: "direct main" description: -- 2.54.0 From 3be13f5589583cb26eced7881cfebb0ed3b1965f Mon Sep 17 00:00:00 2001 From: istalri Date: Tue, 2 Jun 2026 19:14:02 +0200 Subject: [PATCH 2/7] UI/UX improvement for the login flow. You can use enter in all text fields and you get better visual cues like red borders on error. --- lib/controllers/client_state_controller.dart | 8 -- lib/main.dart | 5 +- lib/pages/login_page.dart | 141 ++++++++++++++----- lib/pages/select_server_page.dart | 44 ++++-- 4 files changed, 137 insertions(+), 61 deletions(-) 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)) ], ), ), -- 2.54.0 From 26feae4485d9465a206120e557c8ec8d44b388af Mon Sep 17 00:00:00 2001 From: istalri Date: Tue, 2 Jun 2026 21:43:14 +0200 Subject: [PATCH 3/7] CleanUp Autoformat for better readable code, using autofocus instead of useState and focus element --- lib/controllers/client_controller.dart | 4 +- lib/main.dart | 2 +- lib/pages/login_page.dart | 59 ++--- lib/pages/select_server_page.dart | 295 +++++++++++++------------ 4 files changed, 182 insertions(+), 178 deletions(-) diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index f1ff6a6..fc4a31f 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -263,12 +263,12 @@ class ClientController extends AsyncNotifier { } } - Future discoverHomeserver(Uri homeserver) async { + Future discoverHomeserver(Uri homeserver) async { try { final response = await _sendCommand("discover_homeserver", { "user_id": "@fake-user:${homeserver.host}", }); - return response["m.homeserver"]?["base_url"]; + return Uri.parse(response["m.homeserver"]?["base_url"]); } catch (error) { return null; } diff --git a/lib/main.dart b/lib/main.dart index cb1e53d..dab4e16 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -123,7 +123,7 @@ class App extends StatelessWidget { return Loading(); } - if (!clientState.isLoggedIn) { + if (!clientState.isLoggedIn) { return SelectServerPage(); } else if (!clientState.isVerified) { return VerifyPage(); diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 5d12d00..a321944 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -15,17 +15,8 @@ class LoginPage extends HookConsumerWidget { final client = ref.watch(ClientController.provider.notifier); final isLoggingIn = useState(false); final hasError = useState(false); - final userNameFocusNode = useFocusNode(); final passwordFocusNode = useFocusNode(); - //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(); @@ -40,8 +31,8 @@ class LoginPage extends HookConsumerWidget { LoginRequest( username: username.text, password: password.text, - homeserverUrl: homeserver.origin - ) + homeserverUrl: homeserver.origin, + ), ); if (!context.mounted) return; @@ -52,16 +43,13 @@ class LoginPage extends HookConsumerWidget { SnackBar( content: Text( "Login failed. Is your password right?\nError: $error", - style: TextStyle( - color: theme.colorScheme.onErrorContainer, - ), + style: TextStyle(color: theme.colorScheme.onErrorContainer), ), backgroundColor: theme.colorScheme.errorContainer, ), ); isLoggingIn.value = false; - } - else{ + } else { Navigator.pop(context); } passwordFocusNode.requestFocus(); @@ -72,8 +60,8 @@ class LoginPage extends HookConsumerWidget { appBar: Appbar( leading: IconButton( icon: Icon(Icons.arrow_back), - onPressed: () => Navigator.pop(context) - ), + onPressed: () => Navigator.pop(context), + ), ), body: AlertDialog( title: Text("Login to ${homeserver.host}"), @@ -81,12 +69,10 @@ class LoginPage extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "Enter your login credentials:", - ), + Text("Enter your login credentials:"), SizedBox(height: 12), TextField( - focusNode: userNameFocusNode, + autofocus: true, textInputAction: TextInputAction.next, onChanged: (newVal) { if (hasError.value) { @@ -98,11 +84,11 @@ class LoginPage extends HookConsumerWidget { enabledBorder: OutlineInputBorder( borderSide: BorderSide( width: hasError.value ? 4 : 2, - color: hasError.value - ? theme.colorScheme.error - : theme.colorScheme.primary + color: hasError.value + ? theme.colorScheme.error + : theme.colorScheme.primary, ), - ) + ), ), controller: username, ), @@ -123,29 +109,26 @@ class LoginPage extends HookConsumerWidget { borderSide: BorderSide( width: hasError.value ? 4 : 2, color: hasError.value - ? theme.colorScheme.error - : theme.colorScheme.primary - ) + ? theme.colorScheme.error + : theme.colorScheme.primary, + ), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( width: hasError.value ? 4 : 2, - color: hasError.value - ? theme.colorScheme.error - : theme.colorScheme.primary + color: hasError.value + ? theme.colorScheme.error + : theme.colorScheme.primary, ), - ) + ), ), controller: password, obscureText: true, - ), + ), ], ), actions: [ - TextButton( - onPressed: () => tryLogin(), - child: Text("Sign In"), - ), + TextButton(onPressed: () => tryLogin(), child: Text("Sign In")), ], ), ); diff --git a/lib/pages/select_server_page.dart b/lib/pages/select_server_page.dart index af9be91..c9f4f37 100644 --- a/lib/pages/select_server_page.dart +++ b/lib/pages/select_server_page.dart @@ -20,14 +20,14 @@ class SelectServerPage extends HookConsumerWidget { 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 { isLoading.value = true; - if(newHomeserver?.hasScheme == false){ + if (newHomeserver?.hasScheme == false) { newHomeserver = Uri.https(newHomeserver!.path); } @@ -49,159 +49,180 @@ class SelectServerPage extends HookConsumerWidget { ); } else { homeserverUrl.text = newHomeserver!.origin; - Navigator.push(context, MaterialPageRoute(builder: (_) => LoginPage(homeserver: Uri.parse(newUrl)))); + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => LoginPage(homeserver: Uri.parse(newUrl)), + ), + ); } } - homeserverFocusNode.requestFocus(); - isLoading.value = false; + if (context.mounted) { + homeserverFocusNode.requestFocus(); + isLoading.value = false; + } } return Scaffold( appBar: Appbar(), - body: isLoading.value - ? const Loading() - : Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 600), - child: Column( - children: [ - Row( - children: [ - SvgPicture.asset("assets/icon.svg", width: 128), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + body: isLoading.value + ? const Loading() + : Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: 600), + child: Column( + children: [ + Row( children: [ - Text("Nexus", style: theme.textTheme.displayMedium), - Text( - "A Simple Matrix Client", - style: theme.textTheme.headlineMedium, - overflow: TextOverflow.ellipsis, + SvgPicture.asset("assets/icon.svg", width: 128), + SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Nexus", + style: theme.textTheme.displayMedium, + ), + Text( + "A Simple Matrix Client", + style: theme.textTheme.headlineMedium, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), ], ), - ), - ], - ), - Padding( - padding: EdgeInsetsGeometry.symmetric(vertical: 12), - child: Divider(), - ), - DividerText("Enter a homeserver domain:"), - Row( - spacing: 8, - 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( - width: hasError.value ? 4 : 2, - color: hasError.value - ? theme.colorScheme.error - : theme.colorScheme.primary - ) - ), - ), + Padding( + padding: EdgeInsetsGeometry.symmetric(vertical: 12), + child: Divider(), ), - ), - IconButton.filled( - tooltip: "Confirm homeserver choice", - onPressed: isLoading.value - ? null - : () => setHomeserver(Uri.tryParse(homeserverUrl.text)), - icon: Icon(Icons.check), - ), - ], - ), - Expanded( - child: ListView( - padding: EdgeInsets.only(top: 12), - children: [ - DividerText("Or, choose from some popular homeservers:"), - ...([ - Homeserver( - name: "Matrix.org", - description: - "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", - url: Uri.https("matrix.org"), - iconUrl: - "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", - ), - Homeserver( - name: "Federated Nexus", - description: - "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", - url: Uri.https("federated.nexus"), - iconUrl: "https://federated.nexus/images/icon.png", - ), - Homeserver( - name: "Unredacted", - description: - "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.", - url: Uri.https("unredacted.org", "services/si/matrix"), - iconUrl: "https://unredacted.org/favicon.ico", - ), - Homeserver( - name: "Lorem ipsum", - description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - url: Uri.https("loremipsum.io"), - iconUrl: "https://loremipsum.io/favicon.ico" - ), - ].map( - (homeserver) => Card( - child: ListTile( - title: Text(homeserver.name), - leading: Image.network( - homeserver.iconUrl, - errorBuilder: (_, _, _) => SizedBox.shrink(), - height: 32, - ), - subtitle: Text(homeserver.description), - onTap: isLoading.value - ? null - : () => setHomeserver(homeserver.url), - trailing: IconButton( - tooltip: "Launch homeserver info page", - onPressed: () => launch(homeserver.url), - icon: Icon(Icons.info_outline), + DividerText("Enter a homeserver domain:"), + Row( + spacing: 8, + 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( + width: hasError.value ? 4 : 2, + color: hasError.value + ? theme.colorScheme.error + : theme.colorScheme.primary, + ), + ), ), ), ), - )), + IconButton.filled( + tooltip: "Confirm homeserver choice", + onPressed: isLoading.value + ? null + : () => setHomeserver( + Uri.tryParse(homeserverUrl.text), + ), + icon: Icon(Icons.check), + ), + ], + ), + Expanded( + child: ListView( + padding: EdgeInsets.only(top: 12), + children: [ + DividerText( + "Or, choose from some popular homeservers:", + ), + ...([ + Homeserver( + name: "Matrix.org", + description: + "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.", + url: Uri.https("matrix.org"), + iconUrl: + "https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png", + ), + Homeserver( + name: "Federated Nexus", + description: + "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.", + url: Uri.https("federated.nexus"), + iconUrl: + "https://federated.nexus/images/icon.png", + ), + Homeserver( + name: "Unredacted", + description: + "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.", + url: Uri.https( + "unredacted.org", + "services/si/matrix", + ), + iconUrl: "https://unredacted.org/favicon.ico", + ), + Homeserver( + name: "Lorem ipsum", + description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + url: Uri.https("loremipsum.io"), + iconUrl: "https://loremipsum.io/favicon.ico", + ), + ].map( + (homeserver) => Card( + child: ListTile( + title: Text(homeserver.name), + leading: Image.network( + homeserver.iconUrl, + errorBuilder: (_, _, _) => SizedBox.shrink(), + height: 32, + ), + subtitle: Text(homeserver.description), + onTap: isLoading.value + ? null + : () => setHomeserver(homeserver.url), + trailing: IconButton( + tooltip: "Launch homeserver info page", + onPressed: () => launch(homeserver.url), + icon: Icon(Icons.info_outline), + ), + ), + ), + )), + ], + ), + ), + SizedBox(height: 5), + TextButton( + onPressed: () => + launch(Uri.https("servers.joinmatrix.org")), + child: Text("See more homeservers..."), + ), ], ), ), - SizedBox(height: 5), - TextButton( - onPressed: () => launch(Uri.https("servers.joinmatrix.org")), - child: Text("See more homeservers..."), - ), - ], - ), - ), - ), + ), ); } } -- 2.54.0 From 28be105552b31cb70dbda92dea62f5f769221787 Mon Sep 17 00:00:00 2001 From: istalri Date: Tue, 2 Jun 2026 21:56:43 +0200 Subject: [PATCH 4/7] Fixed inconsistency with focus change in error case on login page. --- lib/pages/login_page.dart | 8 ++++++++ lib/pages/select_server_page.dart | 13 ++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index a321944..3b2b8a6 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -81,6 +81,14 @@ class LoginPage extends HookConsumerWidget { }, decoration: InputDecoration( label: Text("Username"), + 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, diff --git a/lib/pages/select_server_page.dart b/lib/pages/select_server_page.dart index c9f4f37..8a8ac9f 100644 --- a/lib/pages/select_server_page.dart +++ b/lib/pages/select_server_page.dart @@ -49,19 +49,14 @@ class SelectServerPage extends HookConsumerWidget { ); } else { homeserverUrl.text = newHomeserver!.origin; - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => LoginPage(homeserver: Uri.parse(newUrl)), - ), + await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => LoginPage(homeserver: newUrl)), ); } } - if (context.mounted) { - homeserverFocusNode.requestFocus(); - isLoading.value = false; - } + homeserverFocusNode.requestFocus(); + isLoading.value = false; } return Scaffold( -- 2.54.0 From 248b77e4c576646e5bc7c87334e24bfa32907ead Mon Sep 17 00:00:00 2001 From: istalri Date: Fri, 5 Jun 2026 20:02:29 +0200 Subject: [PATCH 5/7] CleanUp and last fixes - Borders only get overwritten in error case - Navigator.pop(context) -> Navigator.of(context).pop() - Confirm homeserver und Sign in button disabled as long as error is active --- lib/pages/login_page.dart | 61 +++++++++++++------------------ lib/pages/select_server_page.dart | 32 ++++++++-------- 2 files changed, 41 insertions(+), 52 deletions(-) diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 3b2b8a6..a432155 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -50,7 +50,7 @@ class LoginPage extends HookConsumerWidget { ); isLoggingIn.value = false; } else { - Navigator.pop(context); + Navigator.of(context).pop(); } passwordFocusNode.requestFocus(); isLoggingIn.value = false; @@ -60,7 +60,7 @@ class LoginPage extends HookConsumerWidget { appBar: Appbar( leading: IconButton( icon: Icon(Icons.arrow_back), - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.of(context).pop(), ), ), body: AlertDialog( @@ -81,22 +81,16 @@ class LoginPage extends HookConsumerWidget { }, decoration: InputDecoration( label: Text("Username"), - 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, - ), - ), + focusedBorder: hasError.value + ? OutlineInputBorder( + borderSide: BorderSide(color: theme.colorScheme.error), + ) + : null, + enabledBorder: hasError.value + ? OutlineInputBorder( + borderSide: BorderSide(color: theme.colorScheme.error), + ) + : null, ), controller: username, ), @@ -113,22 +107,16 @@ class LoginPage extends HookConsumerWidget { 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, - ), - ), + focusedBorder: hasError.value + ? OutlineInputBorder( + borderSide: BorderSide(color: theme.colorScheme.error), + ) + : null, + enabledBorder: hasError.value + ? OutlineInputBorder( + borderSide: BorderSide(color: theme.colorScheme.error), + ) + : null, ), controller: password, obscureText: true, @@ -136,7 +124,10 @@ class LoginPage extends HookConsumerWidget { ], ), actions: [ - TextButton(onPressed: () => tryLogin(), child: Text("Sign In")), + TextButton( + onPressed: hasError.value ? null : () => tryLogin(), + child: Text("Sign In"), + ), ], ), ); diff --git a/lib/pages/select_server_page.dart b/lib/pages/select_server_page.dart index 8a8ac9f..9ef1d49 100644 --- a/lib/pages/select_server_page.dart +++ b/lib/pages/select_server_page.dart @@ -113,28 +113,26 @@ class SelectServerPage extends HookConsumerWidget { 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( - width: hasError.value ? 4 : 2, - color: hasError.value - ? theme.colorScheme.error - : theme.colorScheme.primary, - ), - ), + focusedBorder: hasError.value + ? OutlineInputBorder( + borderSide: BorderSide( + color: theme.colorScheme.error, + ), + ) + : null, + enabledBorder: hasError.value + ? OutlineInputBorder( + borderSide: BorderSide( + color: theme.colorScheme.error, + ), + ) + : null, ), ), ), IconButton.filled( tooltip: "Confirm homeserver choice", - onPressed: isLoading.value + onPressed: isLoading.value || hasError.value ? null : () => setHomeserver( Uri.tryParse(homeserverUrl.text), -- 2.54.0 From b11e64c985254ce4a2fc220f5b7f06d6bdafd5b2 Mon Sep 17 00:00:00 2001 From: istalri Date: Fri, 5 Jun 2026 20:23:25 +0200 Subject: [PATCH 6/7] Removed Lorem ipsum homeserver --- lib/pages/select_server_page.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/pages/select_server_page.dart b/lib/pages/select_server_page.dart index 9ef1d49..cd9ef83 100644 --- a/lib/pages/select_server_page.dart +++ b/lib/pages/select_server_page.dart @@ -175,13 +175,6 @@ class SelectServerPage extends HookConsumerWidget { ), iconUrl: "https://unredacted.org/favicon.ico", ), - Homeserver( - name: "Lorem ipsum", - description: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - url: Uri.https("loremipsum.io"), - iconUrl: "https://loremipsum.io/favicon.ico", - ), ].map( (homeserver) => Card( child: ListTile( -- 2.54.0 From 9ea6cd07a75263e88b54bd684d77acd56163a7a7 Mon Sep 17 00:00:00 2001 From: istalri Date: Fri, 5 Jun 2026 20:55:51 +0200 Subject: [PATCH 7/7] Return function instead of wrapping in annoymous callback --- lib/pages/login_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index a432155..38941e2 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -60,7 +60,7 @@ class LoginPage extends HookConsumerWidget { appBar: Appbar( leading: IconButton( icon: Icon(Icons.arrow_back), - onPressed: () => Navigator.of(context).pop(), + onPressed: Navigator.of(context).pop, ), ), body: AlertDialog( @@ -125,7 +125,7 @@ class LoginPage extends HookConsumerWidget { ), actions: [ TextButton( - onPressed: hasError.value ? null : () => tryLogin(), + onPressed: hasError.value ? null : tryLogin, child: Text("Sign In"), ), ], -- 2.54.0