feat: propose homeserver based on response time

- use `package:matrix_homeserver_recommendations` to benchmark
  homeservers
- propose fastest server without anti-features as homeserver
- display small button with server information
- use Matrix.org / the default configuration as fallback

Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
TheOneWithTheBraid 2022-02-05 00:44:18 +01:00
parent 4b17b1651b
commit 1e194175da
6 changed files with 269 additions and 21 deletions

View File

@ -2774,5 +2774,16 @@
"widgetName": "Name",
"widgetUrlError": "This is not a valid URL.",
"widgetNameError": "Please provide a display name.",
"errorAddingWidget": "Error adding the widget."
"errorAddingWidget": "Error adding the widget.",
"reportUser": "Report user",
"showAvailableHomeservers": "Show available homeservers (Advanced users)",
"antiFeatures": "Anti-features",
"noAntiFeaturesRecorded": "No anti features recorded",
"serverRules": "Server rules",
"jurisdiction": "Jurisdiction",
"selectServer": "Select server",
"responseTime": "Response time",
"openServerList": "Visit",
"reportServerListProblem": "Report list issue",
"serverListJoinMatrix": "Server list by joinMatrix.org"
}

View File

@ -3,10 +3,12 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_web_auth/flutter_web_auth.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
import 'package:universal_html/html.dart' as html;
import 'package:vrouter/vrouter.dart';
@ -16,6 +18,7 @@ import 'package:fluffychat/utils/famedlysdk_store.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/localized_exception_extension.dart';
import 'homeserver_tile.dart';
class HomeserverPicker extends StatefulWidget {
const HomeserverPicker({Key? key}) : super(key: key);
@ -25,14 +28,19 @@ class HomeserverPicker extends StatefulWidget {
}
class HomeserverPickerController extends State<HomeserverPicker> {
bool isLoading = false;
String domain = AppConfig.defaultHomeserver;
final TextEditingController homeserverController =
TextEditingController(text: AppConfig.defaultHomeserver);
bool isLoading = true;
String? domain;
List<HomeserverBenchmarkResult>? benchmarkResults;
TextEditingController? homeserverController;
StreamSubscription? _intentDataStreamSubscription;
String? error;
Timer? _coolDown;
late HomeserverListProvider parser;
void setDomain(String domain) {
this.domain = domain;
_coolDown?.cancel();
@ -67,6 +75,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
void initState() {
super.initState();
checkHomeserverAction();
benchmarkHomeServers();
}
@override
@ -84,10 +93,12 @@ class HomeserverPickerController extends State<HomeserverPicker> {
Future<void> checkHomeserverAction() async {
_coolDown?.cancel();
if (_lastCheckedHomeserver == domain) return;
if (domain.isEmpty) throw L10n.of(context)!.changeTheHomeserver;
if (domain == null || domain!.isEmpty) {
throw L10n.of(context)!.changeTheHomeserver;
}
var homeserver = domain;
if (!homeserver.startsWith('https://')) {
if (!homeserver!.startsWith('https://')) {
homeserver = 'https://$homeserver';
}
@ -179,7 +190,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
void signUpAction() => VRouter.of(context).to(
'signup',
queryParameters: {'domain': domain},
queryParameters: {'domain': domain!},
);
@override
@ -187,6 +198,75 @@ class HomeserverPickerController extends State<HomeserverPicker> {
Matrix.of(context).navigatorContext = context;
return HomeserverPickerView(this);
}
Future<void> benchmarkHomeServers() async {
try {
parser = JoinmatrixOrgParser();
final homeserverList = await parser.fetchHomeservers();
final benchmark = await HomeserverListProvider.benchmarkHomeserver(
homeserverList,
timeout: const Duration(seconds: 10),
// TODO: do not rely on the homeserver list information telling the server supports registration
);
if (benchmark.isEmpty) {
throw NullThrownError();
}
// trying to use server without anti-features
final goodServers = <HomeserverBenchmarkResult>[];
final badServers = <HomeserverBenchmarkResult>[];
for (final result in benchmark) {
if (result.homeserver.antiFeatures == null) {
goodServers.add(result);
} else {
badServers.add(result);
}
}
goodServers.sort();
badServers.sort();
benchmarkResults = List.from([...goodServers, ...badServers]);
domain = benchmarkResults!.first.homeserver.baseUrl.host;
} on Exception catch (e, s) {
Logs().e('Homeserver benchmark failed', e, s);
domain = AppConfig.defaultHomeserver;
} finally {
homeserverController = TextEditingController(text: domain);
checkHomeserverAction();
}
}
Future<void> showServerPicker() async {
final selection = await showModal(
context: context,
builder: (context) => SimpleDialog(
title: Text(L10n.of(context)!.changeTheHomeserver),
children: [
...benchmarkResults!.map<Widget>(
(e) => HomeserverTile(
benchmark: e,
onSelect: () {
Navigator.of(context).pop(e.homeserver);
},
),
),
const Divider(),
JoinMatrixAttributionTile(),
]),
);
if (selection is Homeserver) {
if (domain != selection.baseUrl.host) {
setState(() {
domain = selection.baseUrl.host;
homeserverController!.text = domain!;
});
checkHomeserverAction();
}
}
}
}
class IdentityProvider {

View File

@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
@ -25,18 +26,44 @@ class HomeserverPickerView extends StatelessWidget {
child: Scaffold(
appBar: AppBar(
titleSpacing: 8,
title: DefaultAppBarSearchField(
prefixText: 'https://',
hintText: L10n.of(context)!.enterYourHomeserver,
searchController: controller.homeserverController,
suffix: const Icon(Icons.edit_outlined),
padding: EdgeInsets.zero,
onChanged: controller.setDomain,
readOnly: !AppConfig.allowOtherHomeservers,
onSubmit: (_) => controller.checkHomeserverAction(),
unfocusOnClear: false,
autocorrect: false,
labelText: L10n.of(context)!.homeserver,
title: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation,
) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.vertical,
fillColor: Colors.transparent,
child: child,
);
},
child: controller.homeserverController == null
? Center(
key: ValueKey(controller.homeserverController),
child: const CircularProgressIndicator(),
)
: DefaultAppBarSearchField(
key: ValueKey(controller.homeserverController),
prefixIcon: IconButton(
icon: const Icon(Icons.format_list_numbered),
onPressed: controller.showServerPicker,
tooltip: L10n.of(context)!.showAvailableHomeservers,
),
prefixText: 'https://',
hintText: L10n.of(context)!.enterYourHomeserver,
searchController: controller.homeserverController,
suffix: const Icon(Icons.edit_outlined),
padding: EdgeInsets.zero,
onChanged: controller.setDomain,
readOnly: !AppConfig.allowOtherHomeservers,
onSubmit: (_) => controller.checkHomeserverAction(),
unfocusOnClear: false,
autocorrect: false,
labelText: L10n.of(context)!.homeserver,
),
),
elevation: 0,
),

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix_homeserver_recommendations/matrix_homeserver_recommendations.dart';
import 'package:url_launcher/link.dart';
class HomeserverTile extends StatelessWidget {
final HomeserverBenchmarkResult benchmark;
final VoidCallback onSelect;
const HomeserverTile(
{Key? key, required this.benchmark, required this.onSelect})
: super(key: key);
@override
Widget build(BuildContext context) {
return ExpansionTile(
title: Text(benchmark.homeserver.baseUrl.host),
subtitle: benchmark.homeserver.description != null
? Text(benchmark.homeserver.description!)
: null,
children: [
benchmark.homeserver.antiFeatures != null &&
benchmark.homeserver.antiFeatures!.isNotEmpty
? ListTile(
leading: const Icon(Icons.thumb_down),
title: Text(benchmark.homeserver.antiFeatures!),
subtitle: Text(L10n.of(context)!.antiFeatures),
)
: ListTile(
leading: const Icon(Icons.recommend),
title: Text(L10n.of(context)!.noAntiFeaturesRecorded),
),
if (benchmark.homeserver.jurisdiction != null)
ListTile(
leading: const Icon(Icons.public),
title: Text(benchmark.homeserver.jurisdiction!),
subtitle: Text(L10n.of(context)!.jurisdiction),
),
ListTile(
leading: const Icon(Icons.speed),
title: Text("${benchmark.responseTime!.inMilliseconds} ms"),
subtitle: Text(L10n.of(context)!.responseTime),
),
ButtonBar(
/* spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.end,
runAlignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center, */
children: [
if (benchmark.homeserver.privacyPolicy != null)
Link(
uri: benchmark.homeserver.privacyPolicy!,
target: LinkTarget.blank,
builder: (context, callback) => TextButton(
onPressed: callback,
child: Text(L10n.of(context)!.privacy),
),
),
if (benchmark.homeserver.rules != null)
Link(
uri: benchmark.homeserver.rules!,
target: LinkTarget.blank,
builder: (context, callback) => TextButton(
onPressed: callback,
child: Text(L10n.of(context)!.serverRules),
),
),
OutlinedButton(
onPressed: onSelect.call,
child: Text(L10n.of(context)!.selectServer),
),
],
),
],
);
}
}
class JoinMatrixAttributionTile extends StatelessWidget {
final parser = JoinmatrixOrgParser();
JoinMatrixAttributionTile({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(L10n.of(context)!.serverListJoinMatrix),
subtitle: ButtonBar(children: [
Link(
uri: parser.externalUri,
target: LinkTarget.blank,
builder: (context, callback) => TextButton(
onPressed: callback,
child: Text(L10n.of(context)!.openServerList),
),
),
Link(
uri: parser.errorReportUrl,
target: LinkTarget.blank,
builder: (context, callback) => TextButton(
onPressed: callback,
child: Text(L10n.of(context)!.reportServerListProblem),
),
),
]),
);
}
}

View File

@ -991,6 +991,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:
@ -1697,7 +1704,21 @@ packages:
name: unifiedpush
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "4.0.0"
unifiedpush_android:
dependency: transitive
description:
name: unifiedpush_android
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
unifiedpush_platform_interface:
dependency: transitive
description:
name: unifiedpush_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
universal_html:
dependency: "direct main"
description:

View File

@ -59,6 +59,7 @@ dependencies:
localstorage: ^4.0.0+1
lottie: ^1.2.2
matrix: ^0.8.17
matrix_homeserver_recommendations: ^0.2.0
matrix_link_text: ^1.0.2
native_imaging:
git: https://gitlab.com/famedly/libraries/native_imaging.git