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
4 changed files with 181 additions and 177 deletions
Showing only changes of commit 26feae4485 - Show all commits
Autoformat for better readable code,
using autofocus instead of useState and focus element
istalri 2026-06-02 21:43:14 +02:00

View file

@ -263,12 +263,12 @@ class ClientController extends AsyncNotifier<int> {
}
}
Future<String?> discoverHomeserver(Uri homeserver) async {
Future<Uri?> 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;
}

View file

@ -123,7 +123,7 @@ class App extends StatelessWidget {
return Loading();
}
if (!clientState.isLoggedIn) {
if (!clientState.isLoggedIn) {
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.isVerified) {
return VerifyPage();

View file

@ -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();

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?
//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);
istalri marked this conversation as resolved

prefer Navigator.of(context).pop()

prefer `Navigator.of(context).pop()`
}
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),
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, ```
),
),
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) {
@ -99,10 +85,10 @@ 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,
),
)
),
),
controller: username,
),
@ -123,18 +109,18 @@ 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
? theme.colorScheme.error
: theme.colorScheme.primary,
),
)
),
),
controller: password,
obscureText: true,
@ -142,10 +128,7 @@ class LoginPage extends HookConsumerWidget {
],
),
actions: [
TextButton(
onPressed: () => tryLogin(),
child: Text("Sign In"),
),
TextButton(onPressed: () => tryLogin(), child: Text("Sign In")),
],
),
);

View file

@ -27,7 +27,7 @@ class SelectServerPage extends HookConsumerWidget {
Future<void> 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,
? 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>[
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>[
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..."),
),
],
),
),
),
),
);
}
}