Server selection: now only the cards are scrollable and some cleanup

This commit is contained in:
istalri 2026-06-02 09:29:52 +02:00
commit 11c85f4a07
5 changed files with 247 additions and 173 deletions

View file

@ -9,6 +9,14 @@ class ClientStateController extends Notifier<ClientState?> {
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, 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();
}
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();

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";
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) {
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(
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,
),
);
@ -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);
}
}
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)
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: