diff --git a/lib/pages/homeserver_picker/homeserver_bottom_sheet.dart b/lib/pages/homeserver_picker/homeserver_bottom_sheet.dart new file mode 100644 index 00000000..316d1cf2 --- /dev/null +++ b/lib/pages/homeserver_picker/homeserver_bottom_sheet.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class HomeserverBottomSheet extends StatelessWidget { + final HomeserverBenchmarkResult homeserver; + const HomeserverBottomSheet({required this.homeserver, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final responseTime = homeserver.responseTime; + final description = homeserver.homeserver.description; + final rules = homeserver.homeserver.rules; + final privacy = homeserver.homeserver.privacyPolicy; + final registration = homeserver.homeserver.registration; + final jurisdiction = homeserver.homeserver.jurisdiction; + final homeserverSoftware = homeserver.homeserver.homeserverSoftware; + return Scaffold( + appBar: AppBar( + title: Text(homeserver.homeserver.baseUrl.host), + ), + body: ListView(children: [ + if (description != null && description.isNotEmpty) + ListTile( + leading: const Icon(Icons.info_outlined), + title: Text(description), + ), + if (jurisdiction != null && jurisdiction.isNotEmpty) + ListTile( + leading: const Icon(Icons.location_city_outlined), + title: Text(jurisdiction), + ), + if (homeserverSoftware != null && homeserverSoftware.isNotEmpty) + ListTile( + leading: const Icon(Icons.domain_outlined), + title: Text(homeserverSoftware), + ), + ListTile( + onTap: () => launch(homeserver.homeserver.baseUrl.toString()), + leading: const Icon(Icons.link_outlined), + title: Text(homeserver.homeserver.baseUrl.toString()), + ), + if (registration != null) + ListTile( + onTap: () => launch(registration.toString()), + leading: const Icon(Icons.person_add_outlined), + title: Text(registration.toString()), + ), + if (rules != null) + ListTile( + onTap: () => launch(rules.toString()), + leading: const Icon(Icons.visibility_outlined), + title: Text(rules.toString()), + ), + if (privacy != null) + ListTile( + onTap: () => launch(privacy.toString()), + leading: const Icon(Icons.shield_outlined), + title: Text(privacy.toString()), + ), + if (responseTime != null) + ListTile( + leading: const Icon(Icons.timer_outlined), + title: Text('${responseTime.inMilliseconds}ms'), + ), + ]), + ); + } +} diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index c85cc529..d0533c29 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; +import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/homeserver_picker/homeserver_bottom_sheet.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../../utils/localized_exception_extension.dart'; @@ -19,9 +21,62 @@ class HomeserverPicker extends StatefulWidget { class HomeserverPickerController extends State { bool isLoading = false; - final TextEditingController homeserverController = - TextEditingController(text: AppConfig.defaultHomeserver); + final TextEditingController homeserverController = TextEditingController( + text: AppConfig.defaultHomeserver, + ); + final FocusNode homeserverFocusNode = FocusNode(); String? error; + List? benchmarkResults; + bool displayServerList = false; + bool get loadingHomeservers => + AppConfig.allowOtherHomeservers && benchmarkResults == null; + String searchTerm = ''; + + void _updateFocus() { + if (benchmarkResults == null) _loadHomeserverList(); + setState(() { + displayServerList = homeserverFocusNode.hasFocus; + }); + } + + void showServerInfo(HomeserverBenchmarkResult server) => showModalBottomSheet( + context: context, + builder: (_) => HomeserverBottomSheet( + homeserver: server, + ), + ); + + void onChanged(String text) => setState(() { + searchTerm = text; + }); + + List get filteredHomeservers => benchmarkResults! + .where((element) => + element.homeserver.baseUrl.host.contains(searchTerm) || + (element.homeserver.description?.contains(searchTerm) ?? false)) + .toList(); + + void _loadHomeserverList() async { + try { + final homeserverList = await JoinmatrixOrgParser().fetchHomeservers(); + final benchmark = await HomeserverListProvider.benchmarkHomeserver( + homeserverList, + timeout: const Duration(seconds: 10), + ); + setState(() { + benchmarkResults = benchmark; + }); + } catch (e, s) { + Logs().e('Homeserver benchmark failed', e, s); + benchmarkResults = []; + } + } + + void setServer(String server) => setState(() { + homeserverController.text = server; + searchTerm = ''; + homeserverFocusNode.unfocus(); + }); /// Starts an analysis of the given homeserver. It uses the current domain and /// makes sure that it is prefixed with https. Then it searches for the @@ -29,8 +84,11 @@ class HomeserverPickerController extends State { /// login type. Future checkHomeserverAction() async { setState(() { + homeserverFocusNode.unfocus(); error = null; isLoading = true; + searchTerm = ''; + displayServerList = false; }); try { @@ -68,6 +126,18 @@ class HomeserverPickerController extends State { } } + @override + void dispose() { + homeserverFocusNode.removeListener(_updateFocus); + super.dispose(); + } + + @override + void initState() { + homeserverFocusNode.addListener(_updateFocus); + super.initState(); + } + @override Widget build(BuildContext context) { Matrix.of(context).navigatorContext = context; diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index d6ec147a..e24b136c 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -17,6 +17,7 @@ class HomeserverPickerView extends StatelessWidget { @override Widget build(BuildContext context) { + final benchmarkResults = controller.benchmarkResults; return LoginScaffold( appBar: VRouter.of(context).path == '/home' ? null @@ -26,18 +27,19 @@ class HomeserverPickerView extends StatelessWidget { Expanded( child: ListView( children: [ - Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 256), - child: Image.asset( - 'assets/info-logo.png', - ), - ), + AnimatedContainer( + duration: const Duration(milliseconds: 300), + constraints: BoxConstraints( + maxHeight: controller.displayServerList ? 0 : 256), + alignment: Alignment.center, + child: Image.asset('assets/info-logo.png'), ), Padding( padding: const EdgeInsets.all(16.0), child: TextField( + focusNode: controller.homeserverFocusNode, controller: controller.homeserverController, + onChanged: controller.onChanged, decoration: FluffyThemes.loginTextFieldDecoration( labelText: L10n.of(context)!.homeserver, hintText: L10n.of(context)!.enterYourHomeserver, @@ -49,6 +51,42 @@ class HomeserverPickerView extends StatelessWidget { autocorrect: false, ), ), + if (controller.displayServerList) + Padding( + padding: const EdgeInsets.all(16.0), + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + color: Colors.white.withAlpha(200), + clipBehavior: Clip.hardEdge, + child: benchmarkResults == null + ? const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator.adaptive(), + )) + : Column( + children: controller.filteredHomeservers + .map( + (server) => ListTile( + trailing: IconButton( + icon: const Icon(Icons.info_outlined), + onPressed: () => + controller.showServerInfo(server), + ), + onTap: () => controller.setServer( + server.homeserver.baseUrl.host), + title: Text( + server.homeserver.baseUrl.host, + ), + subtitle: Text( + server.homeserver.description ?? ''), + ), + ) + .toList(), + ), + ), + ), Wrap( alignment: WrapAlignment.center, children: [ diff --git a/pubspec.lock b/pubspec.lock index d2a25589..3c014a2f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1019,6 +1019,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.5.3" + matrix_homeserver_recommendations: + dependency: "direct main" + description: + name: matrix_homeserver_recommendations + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" matrix_link_text: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a4c10de3..8569f5a2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,7 @@ dependencies: localstorage: ^4.0.0+1 lottie: ^1.2.2 matrix: ^0.8.20 + matrix_homeserver_recommendations: ^0.2.0 matrix_link_text: ^1.0.2 native_imaging: git: https://gitlab.com/famedly/company/frontend/libraries/native_imaging.git