From 448a111c4890b453e4ff5341895979d60e6918c2 Mon Sep 17 00:00:00 2001 From: Krille Date: Sun, 3 Nov 2024 16:58:04 +0100 Subject: [PATCH] feat: Add about server page --- assets/l10n/intl_en.arb | 16 +- lib/config/routes.dart | 12 + lib/pages/settings/settings_view.dart | 12 +- .../settings_homeserver.dart | 58 ++++ .../settings_homeserver_view.dart | 295 ++++++++++++++++++ lib/utils/localized_exception_extension.dart | 5 + 6 files changed, 391 insertions(+), 7 deletions(-) create mode 100644 lib/pages/settings_homeserver/settings_homeserver.dart create mode 100644 lib/pages/settings_homeserver/settings_homeserver_view.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 00325b0ca..9e67e4875 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -25,9 +25,12 @@ "replace": "Replace", "@replace": {}, "about": "About", - "@about": { + "aboutHomeserver": "About {homeserver}", + "@aboutHomeserver": { "type": "text", - "placeholders": {} + "placeholders": { + "homeserver": {} + } }, "accept": "Accept", "@accept": { @@ -2794,5 +2797,12 @@ "blur": "Blur:", "opacity": "Opacity:", "setWallpaper": "Set wallpaper", - "manageAccount": "Manage account" + "manageAccount": "Manage account", + "noContactInformationProvided": "Server does not provide any valid contact information", + "contactServerAdmin": "Contact server admin", + "contactServerSecurity": "Contact server security", + "supportPage": "Support page", + "serverInformation": "Server information:", + "name": "Name", + "version": "Version" } diff --git a/lib/config/routes.dart b/lib/config/routes.dart index f12b27fdd..8c508790f 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -24,6 +24,7 @@ import 'package:fluffychat/pages/settings/settings.dart'; import 'package:fluffychat/pages/settings_3pid/settings_3pid.dart'; import 'package:fluffychat/pages/settings_chat/settings_chat.dart'; import 'package:fluffychat/pages/settings_emotes/settings_emotes.dart'; +import 'package:fluffychat/pages/settings_homeserver/settings_homeserver.dart'; import 'package:fluffychat/pages/settings_ignore_list/settings_ignore_list.dart'; import 'package:fluffychat/pages/settings_multiple_emotes/settings_multiple_emotes.dart'; import 'package:fluffychat/pages/settings_notifications/settings_notifications.dart'; @@ -255,6 +256,17 @@ abstract class AppRoutes { ), ], ), + GoRoute( + path: 'homeserver', + pageBuilder: (context, state) { + return defaultPageBuilder( + context, + state, + const SettingsHomeserver(), + ); + }, + redirect: loggedOutRedirect, + ), GoRoute( path: 'security', redirect: loggedOutRedirect, diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 40934653f..00b8fe7c2 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -175,12 +175,16 @@ class SettingsView extends StatelessWidget { ), Divider(color: theme.dividerColor), ListTile( - leading: const Icon(Icons.help_outline_outlined), - title: Text(L10n.of(context).help), - onTap: () => launchUrlString(AppConfig.supportUrl), + leading: const Icon(Icons.dns_outlined), + title: Text( + L10n.of(context).aboutHomeserver( + Matrix.of(context).client.userID?.domain ?? 'homeserver', + ), + ), + onTap: () => context.go('/rooms/settings/homeserver'), ), ListTile( - leading: const Icon(Icons.shield_sharp), + leading: const Icon(Icons.privacy_tip_outlined), title: Text(L10n.of(context).privacy), onTap: () => launchUrlString(AppConfig.privacyUrl), ), diff --git a/lib/pages/settings_homeserver/settings_homeserver.dart b/lib/pages/settings_homeserver/settings_homeserver.dart new file mode 100644 index 000000000..76ca08232 --- /dev/null +++ b/lib/pages/settings_homeserver/settings_homeserver.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import 'package:http/http.dart' as http; +import 'package:matrix/matrix.dart'; + +import '../../widgets/matrix.dart'; +import 'settings_homeserver_view.dart'; + +class SettingsHomeserver extends StatefulWidget { + const SettingsHomeserver({super.key}); + + @override + SettingsHomeserverController createState() => SettingsHomeserverController(); +} + +class SettingsHomeserverController extends State { + Future<({String name, String version, Uri federationBaseUrl})> + fetchServerInfo() async { + final client = Matrix.of(context).client; + final domain = client.userID!.domain!; + final httpClient = client.httpClient; + var federationBaseUrl = Uri(host: domain, port: 8448, scheme: 'https'); + try { + final serverWellKnownResult = await httpClient.get( + Uri.https(domain, '/.well-known/matrix/server'), + ); + final serverWellKnown = jsonDecode(serverWellKnownResult.body); + federationBaseUrl = Uri.https(serverWellKnown['m.server']); + } catch (e, s) { + Logs().w( + 'Unable to fetch federation base uri. Use $federationBaseUrl', + e, + s, + ); + } + + final serverVersionResult = await http.get( + federationBaseUrl.resolveUri( + Uri(path: '/_matrix/federation/v1/version'), + ), + ); + final { + 'server': { + 'name': String name, + 'version': String version, + }, + } = Map>.from( + jsonDecode(serverVersionResult.body), + ); + + return (name: name, version: version, federationBaseUrl: federationBaseUrl); + } + + @override + Widget build(BuildContext context) => SettingsHomeserverView(this); +} diff --git a/lib/pages/settings_homeserver/settings_homeserver_view.dart b/lib/pages/settings_homeserver/settings_homeserver_view.dart new file mode 100644 index 000000000..792d6093e --- /dev/null +++ b/lib/pages/settings_homeserver/settings_homeserver_view.dart @@ -0,0 +1,295 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:matrix/matrix.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/widgets/layouts/max_width_body.dart'; +import '../../widgets/matrix.dart'; +import 'settings_homeserver.dart'; + +class SettingsHomeserverView extends StatelessWidget { + final SettingsHomeserverController controller; + + const SettingsHomeserverView(this.controller, {super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final client = Matrix.of(context).client; + + return Scaffold( + appBar: AppBar( + leading: const Center(child: BackButton()), + title: Text( + L10n.of(context) + .aboutHomeserver(client.userID?.domain ?? 'Homeserver'), + ), + ), + body: MaxWidthBody( + withScrolling: false, + child: SelectionArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + L10n.of(context).serverInformation, + style: TextStyle( + color: theme.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + FutureBuilder( + future: client.getWellknownSupport(), + builder: (context, snapshot) { + final error = snapshot.error; + final data = snapshot.data; + if (error != null) { + return ListTile( + leading: const Icon(Icons.error_outlined), + title: Text( + error.toLocalizedString( + context, + ExceptionContext.checkServerSupportInfo, + ), + style: const TextStyle(fontSize: 14), + ), + ); + } + if (data == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + final supportPage = data.supportPage; + final contacts = data.contacts; + if (supportPage == null && contacts == null) { + return ListTile( + leading: const Icon(Icons.error_outlined), + title: Text( + L10n.of(context).noContactInformationProvided, + style: const TextStyle(fontSize: 14), + ), + ); + } + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (supportPage != null) + ListTile( + title: Text(L10n.of(context).supportPage), + subtitle: Text(supportPage), + ), + if (contacts != null) + ...contacts.map( + (contact) { + return ListTile( + title: Text( + contact.role.localizedString( + L10n.of(context), + ), + ), + subtitle: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (contact.emailAddress != null) + TextButton( + onPressed: () {}, + child: Text(contact.emailAddress!), + ), + if (contact.matrixId != null) + TextButton( + onPressed: () {}, + child: Text(contact.matrixId!), + ), + ], + ), + ); + }, + ), + ], + ); + }, + ), + FutureBuilder( + future: controller.fetchServerInfo(), + builder: (context, snapshot) { + final error = snapshot.error; + if (error != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outlined, + color: theme.colorScheme.error, + ), + const SizedBox(height: 12), + Text( + error.toLocalizedString(context), + textAlign: TextAlign.center, + style: TextStyle( + color: theme.colorScheme.error, + ), + ), + ], + ); + } + final data = snapshot.data; + if (data == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text(L10n.of(context).name), + subtitle: Text(data.name), + ), + ListTile( + title: Text(L10n.of(context).version), + subtitle: Text(data.version), + ), + ListTile( + title: const Text('Federation Base URL'), + subtitle: Linkify( + text: data.federationBaseUrl.toString(), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: theme.colorScheme.primary, + decorationColor: theme.colorScheme.primary, + ), + onOpen: (link) => launchUrlString(link.url), + ), + ), + ], + ); + }, + ), + Divider(color: theme.dividerColor), + FutureBuilder( + future: client.getWellknown(), + initialData: client.wellKnown, + builder: (context, snapshot) { + final error = snapshot.error; + if (error != null) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outlined, + color: theme.colorScheme.error, + ), + const SizedBox(height: 12), + Text( + error.toLocalizedString(context), + textAlign: TextAlign.center, + style: TextStyle( + color: theme.colorScheme.error, + ), + ), + ], + ); + } + final wellKnown = snapshot.data; + if (wellKnown == null) { + return const Center( + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ); + } + final identityServer = wellKnown.mIdentityServer; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + 'Client-Well-Known Information:', + style: TextStyle( + color: theme.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + title: const Text('Base URL'), + subtitle: Linkify( + text: wellKnown.mHomeserver.baseUrl.toString(), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: theme.colorScheme.primary, + decorationColor: theme.colorScheme.primary, + ), + onOpen: (link) => launchUrlString(link.url), + ), + ), + if (identityServer != null) + ListTile( + title: const Text('Identity Server:'), + subtitle: Linkify( + text: identityServer.baseUrl.toString(), + options: const LinkifyOptions(humanize: false), + linkStyle: TextStyle( + color: theme.colorScheme.primary, + decorationColor: theme.colorScheme.primary, + ), + onOpen: (link) => launchUrlString(link.url), + ), + ), + ...wellKnown.additionalProperties.entries.map( + (entry) => ListTile( + title: Text(entry.key), + subtitle: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + color: theme.colorScheme.surfaceContainer, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + scrollDirection: Axis.horizontal, + child: Text( + const JsonEncoder.withIndent(' ') + .convert(entry.value), + style: TextStyle( + color: theme.colorScheme.onSurface, + ), + ), + ), + ), + ), + ), + ], + ); + }, + ), + ], + ), + ), + ), + ); + } +} + +extension on Role { + String localizedString(L10n l10n) { + switch (this) { + case Role.mRoleAdmin: + return l10n.contactServerAdmin; + case Role.mRoleSecurity: + return l10n.contactServerSecurity; + } + } +} diff --git a/lib/utils/localized_exception_extension.dart b/lib/utils/localized_exception_extension.dart index 8c1c3a53a..4716dcbdb 100644 --- a/lib/utils/localized_exception_extension.dart +++ b/lib/utils/localized_exception_extension.dart @@ -95,6 +95,10 @@ extension LocalizedExceptionExtension on Object { exceptionContext == ExceptionContext.checkHomeserver) { return L10n.of(context).doesNotSeemToBeAValidHomeserver; } + if (this is FormatException && + exceptionContext == ExceptionContext.checkServerSupportInfo) { + return L10n.of(context).noContactInformationProvided; + } if (this is String) return toString(); if (this is UiaException) return toString(); Logs().w('Something went wrong: ', this); @@ -105,4 +109,5 @@ extension LocalizedExceptionExtension on Object { enum ExceptionContext { changePassword, checkHomeserver, + checkServerSupportInfo, }