Server selection and login are on different pages. #33

Manually merged
Henry-Hiles merged 8 commits from istalri/nexus:better-login into better-login 2026-06-05 16:44:35 -04:00
5 changed files with 247 additions and 173 deletions
Showing only changes of commit 5ad85d3f62 - Show all commits

Login is on seperated page and for the server selection only the cards instead of the entire page are scrollable.

istalri 2026-06-02 09:29:52 +02:00

View file

@ -9,6 +9,14 @@ class ClientStateController extends Notifier<ClientState?> {
state = newState;
}
void setHomeServer(String homeserver){
Henry-Hiles marked this conversation as resolved Outdated

Given this won't be persistent anyways, I think we can go for a simpler approach and not touch client state controller.

Given this won't be persistent anyways, I think we can go for a simpler approach and not touch client state controller.
state = .new(isInitialized: state?.isInitialized ?? false,
isLoggedIn: state?.isLoggedIn ?? false,
isVerified: state?.isVerified ?? false,
userId: state?.userId,
homeserverUrl: homeserver);
}
static final provider = NotifierProvider<ClientStateController, ClientState?>(
ClientStateController.new,
);

View file

@ -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();
}
Henry-Hiles marked this conversation as resolved Outdated

you changed this to have two spaces between the ) and {, not sure why

you changed this to have two spaces between the ) and {, not sure why

yeah that one was probably an typo

yeah that one was probably an typo
if (!clientState.isLoggedIn) {
if ((!clientState.isLoggedIn) && (clientState.homeserverUrl == null || clientState.homeserverUrl?.isEmpty == true)) {
return SelectServerPage();
Henry-Hiles marked this conversation as resolved Outdated

I don't think this logic needs to be changed this much. If you can keep the previous logic, but instead show ServerSelectPage instead of login, we can take a different approach to push the login.

I don't think this logic needs to be changed this much. If you can keep the previous logic, but instead show ServerSelectPage instead of login, we can take a different approach to push the login.
} else if(!clientState.isLoggedIn) {
return LoginPage();
} else if (!clientState.isVerified) {
return VerifyPage();

View file

@ -1,166 +1,41 @@
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";
Henry-Hiles marked this conversation as resolved

Lets take in final Uri homeserver as an argument here, instead of using ClientStateController.

Lets take in `final Uri homeserver` as an argument here, instead of using `ClientStateController`.
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<String?>(null);
final launch = ref.watch(LaunchHelper.provider).launchUrl;
Future<void> 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) {
Henry-Hiles marked this conversation as resolved Outdated

This will be redundant when it takes in a URI.

This will be redundant when it takes in a URI.
throw Exception("Homeserver URL must be set for login.");

These focusnodes shouldnt be needed. Perhaps we just need a Form()

These focusnodes shouldnt be needed. Perhaps we just need a `Form()`

TODO: This is still open but you said you wanted to look into it. Is this still the case?

TODO: This is still open but you said you wanted to look into it. Is this still the case?
}
Henry-Hiles marked this conversation as resolved Outdated

Uh, no it's not. The "safe"/normal way is to set autofocus on the text field.

Uh, no it's not. The "safe"/normal way is to set `autofocus` on the text field.

Damn, alright learned something new. Stackoverlfow entries are probably too old :D

Damn, alright learned something new. Stackoverlfow entries are probably too old :D
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(
body: AlertDialog(
title: Text("Login"),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Nexus", style: theme.textTheme.displayMedium),
Text(
"A Simple Matrix Client",
style: theme.textTheme.headlineMedium,
overflow: TextOverflow.ellipsis,
"Enter your login credentials:",
),
],
),
),
],
),
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>[
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),
SizedBox(height: 12),
TextField(
decoration: InputDecoration(label: Text("Username")),
controller: username,
@ -171,15 +46,19 @@ class LoginPage extends HookConsumerWidget {
controller: password,
obscureText: true,
),
SizedBox(height: 12),
ElevatedButton(
onPressed: () async {
isLoading.value = 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,
),
);
istalri marked this conversation as resolved Outdated
- onPressed: () => Navigator.pop(context),
+ onPressed: Navigator.of(context).pop,
```diff - onPressed: () => Navigator.pop(context), + onPressed: Navigator.of(context).pop, ```
istalri marked this conversation as resolved Outdated
- onPressed: () => Navigator.of(context).pop(),
+ onPressed: Navigator.of(context).pop,
```diff - onPressed: () => Navigator.of(context).pop(), + onPressed: Navigator.of(context).pop, ```
Henry-Hiles marked this conversation as resolved Outdated

bad formatting here. make sure you format your code :)

bad formatting here. make sure you format your code :)

Yeah I totally forgot. Autoformatting is good? like ctrl + shift + i?

Yeah I totally forgot. Autoformatting is good? like ctrl + shift + i?

Yeah, that should work great. I have it set to format on save.

Yeah, that should work great. I have it set to format on save.
@ -195,15 +74,12 @@ class LoginPage extends HookConsumerWidget {
backgroundColor: theme.colorScheme.errorContainer,
),
);
isLoading.value = false;
isLoggingIn.value = false;
}
},
child: Text("Sign In"),
),
],
],
),
),
),
);
}

View file

@ -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<void> 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);
Henry-Hiles marked this conversation as resolved

Instead of setting homeserver in client state controller, can we just Navigator.of(context).push the LoginPage, passing in a homeserver as an argument?

Instead of setting homeserver in client state controller, can we just `Navigator.of(context).push` the `LoginPage`, passing in a `homeserver` as an argument?
}
Henry-Hiles marked this conversation as resolved Outdated

Might be nice to do that Uri.parse in client.discoverHomeserver, and have that return a Uri.

Might be nice to do that `Uri.parse` in `client.discoverHomeserver`, and have that return a Uri.

Yeah I don't mind. If you feel that's cleaner I will do so.

Yeah I don't mind. If you feel that's cleaner I will do so.

Also, you should use Navigator.of(context).push, and await it.

Also, you should use `Navigator.of(context).push`, and await it.

Makes sense, will do

Makes sense, will do
}
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>[
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)
Henry-Hiles marked this conversation as resolved Outdated

This padding should be removed entirely now theres nothing below it. When loading, the whole page should be replaced by a loading indicator, not just at the bottom.

This padding should be removed entirely now theres nothing below it. When loading, the whole page should be replaced by a loading indicator, not just at the bottom.
Padding(padding: EdgeInsets.only(top: 32), child: Loading())
else
Padding(padding: EdgeInsets.only(top: 12))
],
),
),
),
);
}
}

View file

@ -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: