feat: Implement new sign in flow
This commit is contained in:
parent
f7932639e2
commit
4d7f0295ca
19 changed files with 944 additions and 559 deletions
|
|
@ -1,221 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
|
||||
import 'package:fluffychat/utils/file_selector.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import '../../utils/localized_exception_extension.dart';
|
||||
|
||||
class HomeserverPicker extends StatefulWidget {
|
||||
final bool addMultiAccount;
|
||||
const HomeserverPicker({required this.addMultiAccount, super.key});
|
||||
|
||||
@override
|
||||
HomeserverPickerController createState() => HomeserverPickerController();
|
||||
}
|
||||
|
||||
class HomeserverPickerController extends State<HomeserverPicker> {
|
||||
bool isLoading = false;
|
||||
|
||||
final TextEditingController homeserverController = TextEditingController(
|
||||
text: AppSettings.defaultHomeserver.value,
|
||||
);
|
||||
|
||||
String? error;
|
||||
|
||||
/// 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
|
||||
/// well-known information and forwards to the login page depending on the
|
||||
/// login type.
|
||||
Future<void> checkHomeserverAction({bool legacyPasswordLogin = false}) async {
|
||||
final homeserverInput = homeserverController.text
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replaceAll(' ', '-');
|
||||
|
||||
if (homeserverInput.isEmpty) {
|
||||
final client = await Matrix.of(context).getLoginClient();
|
||||
setState(() {
|
||||
error = loginFlows = null;
|
||||
isLoading = false;
|
||||
client.homeserver = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
error = loginFlows = null;
|
||||
isLoading = true;
|
||||
});
|
||||
|
||||
final l10n = L10n.of(context);
|
||||
|
||||
try {
|
||||
var homeserver = Uri.parse(homeserverInput);
|
||||
if (homeserver.scheme.isEmpty) {
|
||||
homeserver = Uri.https(homeserverInput, '');
|
||||
}
|
||||
final client = await Matrix.of(context).getLoginClient();
|
||||
final (_, _, loginFlows, _) = await client.checkHomeserver(homeserver);
|
||||
this.loginFlows = loginFlows;
|
||||
if (supportsSso && !legacyPasswordLogin) {
|
||||
if (!PlatformInfos.isMobile) {
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: l10n.appWantsToUseForLogin(homeserverInput),
|
||||
message: l10n.appWantsToUseForLoginDescription,
|
||||
okLabel: l10n.continueText,
|
||||
);
|
||||
if (consent != OkCancelResult.ok) return;
|
||||
}
|
||||
return ssoLoginAction();
|
||||
}
|
||||
context.push(
|
||||
'${GoRouter.of(context).routeInformationProvider.value.uri.path}/login',
|
||||
extra: client,
|
||||
);
|
||||
} catch (e) {
|
||||
setState(
|
||||
() => error = (e).toLocalizedString(
|
||||
context,
|
||||
ExceptionContext.checkHomeserver,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<LoginFlow>? loginFlows;
|
||||
|
||||
bool _supportsFlow(String flowType) =>
|
||||
loginFlows?.any((flow) => flow.type == flowType) ?? false;
|
||||
|
||||
bool get supportsSso => _supportsFlow('m.login.sso');
|
||||
|
||||
bool isDefaultPlatform =
|
||||
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS);
|
||||
|
||||
bool get supportsPasswordLogin => _supportsFlow('m.login.password');
|
||||
|
||||
void ssoLoginAction() async {
|
||||
final redirectUrl = kIsWeb
|
||||
? Uri.parse(
|
||||
html.window.location.href,
|
||||
).resolveUri(Uri(pathSegments: ['auth.html'])).toString()
|
||||
: isDefaultPlatform
|
||||
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
|
||||
: 'http://localhost:3001//login';
|
||||
final client = await Matrix.of(context).getLoginClient();
|
||||
final url = client.homeserver!.replace(
|
||||
path: '/_matrix/client/v3/login/sso/redirect',
|
||||
queryParameters: {'redirectUrl': redirectUrl},
|
||||
);
|
||||
|
||||
final urlScheme = isDefaultPlatform
|
||||
? Uri.parse(redirectUrl).scheme
|
||||
: "http://localhost:3001";
|
||||
final result = await FlutterWebAuth2.authenticate(
|
||||
url: url.toString(),
|
||||
callbackUrlScheme: urlScheme,
|
||||
options: FlutterWebAuth2Options(useWebview: PlatformInfos.isMobile),
|
||||
);
|
||||
final token = Uri.parse(result).queryParameters['loginToken'];
|
||||
if (token?.isEmpty ?? false) return;
|
||||
|
||||
setState(() {
|
||||
error = null;
|
||||
isLoading = true;
|
||||
});
|
||||
try {
|
||||
await client.login(
|
||||
LoginType.mLoginToken,
|
||||
token: token,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||
);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toLocalizedString(context);
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => HomeserverPickerView(this);
|
||||
|
||||
Future<void> restoreBackup() async {
|
||||
final picked = await selectFiles(context);
|
||||
final file = picked.firstOrNull;
|
||||
if (file == null) return;
|
||||
setState(() {
|
||||
error = null;
|
||||
isLoading = true;
|
||||
});
|
||||
try {
|
||||
final client = await Matrix.of(context).getLoginClient();
|
||||
await client.importDump(String.fromCharCodes(await file.readAsBytes()));
|
||||
Matrix.of(context).initMatrix();
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
error = e.toLocalizedString(context);
|
||||
});
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onMoreAction(MoreLoginActions action) {
|
||||
switch (action) {
|
||||
case MoreLoginActions.importBackup:
|
||||
restoreBackup();
|
||||
case MoreLoginActions.privacy:
|
||||
launchUrl(AppConfig.privacyUrl);
|
||||
case MoreLoginActions.about:
|
||||
PlatformInfos.showDialog(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MoreLoginActions { importBackup, privacy, about }
|
||||
|
||||
class IdentityProvider {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final String? icon;
|
||||
final String? brand;
|
||||
|
||||
IdentityProvider({this.id, this.name, this.icon, this.brand});
|
||||
|
||||
factory IdentityProvider.fromJson(Map<String, dynamic> json) =>
|
||||
IdentityProvider(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
icon: json['icon'],
|
||||
brand: json['brand'],
|
||||
);
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/adaptive_dialog_action.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'homeserver_picker.dart';
|
||||
|
||||
class HomeserverPickerView extends StatelessWidget {
|
||||
final HomeserverPickerController controller;
|
||||
|
||||
const HomeserverPickerView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return LoginScaffold(
|
||||
enforceMobileMode: Matrix.of(
|
||||
context,
|
||||
).widget.clients.any((client) => client.isLogged()),
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
controller.widget.addMultiAccount
|
||||
? L10n.of(context).addAccount
|
||||
: L10n.of(context).login,
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton<MoreLoginActions>(
|
||||
useRootNavigator: true,
|
||||
onSelected: controller.onMoreAction,
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
value: MoreLoginActions.importBackup,
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
const Icon(Icons.import_export_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).hydrate),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: MoreLoginActions.privacy,
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
const Icon(Icons.privacy_tip_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).privacy),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: MoreLoginActions.about,
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
const Icon(Icons.info_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).about),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Hero(
|
||||
tag: 'info-logo',
|
||||
child: Image.asset(
|
||||
'./assets/banner_transparent.png',
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: SelectableLinkify(
|
||||
text: L10n.of(context).appIntroduction,
|
||||
textScaleFactor: MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(1),
|
||||
textAlign: TextAlign.center,
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.secondary,
|
||||
decorationColor: theme.colorScheme.secondary,
|
||||
),
|
||||
onOpen: (link) => launchUrlString(link.url),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .stretch,
|
||||
children: [
|
||||
TextField(
|
||||
onSubmitted: (_) =>
|
||||
controller.checkHomeserverAction(),
|
||||
controller: controller.homeserverController,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.url,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.search_outlined),
|
||||
filled: false,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
),
|
||||
hintText: AppSettings.defaultHomeserver.value,
|
||||
hintStyle: TextStyle(
|
||||
color: theme.colorScheme.surfaceTint,
|
||||
),
|
||||
labelText: L10n.of(context).signInWithLabel,
|
||||
errorText: controller.error,
|
||||
errorMaxLines: 4,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog.adaptive(
|
||||
title: Text(
|
||||
L10n.of(context).whatIsAHomeserver,
|
||||
),
|
||||
content: Linkify(
|
||||
text: L10n.of(
|
||||
context,
|
||||
).homeserverDescription,
|
||||
textScaleFactor:
|
||||
MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(1),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.primary,
|
||||
decorationColor:
|
||||
theme.colorScheme.primary,
|
||||
),
|
||||
onOpen: (link) =>
|
||||
launchUrlString(link.url),
|
||||
),
|
||||
actions: [
|
||||
AdaptiveDialogAction(
|
||||
onPressed: () => launchUrl(
|
||||
Uri.https('servers.joinmatrix.org'),
|
||||
),
|
||||
child: Text(
|
||||
L10n.of(
|
||||
context,
|
||||
).discoverHomeservers,
|
||||
),
|
||||
),
|
||||
AdaptiveDialogAction(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: Text(L10n.of(context).close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.info_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: theme.colorScheme.onPrimary,
|
||||
),
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
: controller.checkHomeserverAction,
|
||||
child: controller.isLoading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context).continueText),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.secondary,
|
||||
textStyle: theme.textTheme.labelMedium,
|
||||
),
|
||||
onPressed: controller.isLoading
|
||||
? null
|
||||
: () => controller.checkHomeserverAction(
|
||||
legacyPasswordLogin: true,
|
||||
),
|
||||
child: Text(L10n.of(context).loginWithMatrixId),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
21
lib/pages/intro/flows/restore_backup_flow.dart
Normal file
21
lib/pages/intro/flows/restore_backup_flow.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/utils/file_selector.dart';
|
||||
import 'package:fluffychat/widgets/future_loading_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
Future<void> restoreBackupFlow(BuildContext context) async {
|
||||
final picked = await selectFiles(context);
|
||||
final file = picked.firstOrNull;
|
||||
if (file == null) return;
|
||||
|
||||
if (!context.mounted) return;
|
||||
await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () async {
|
||||
final client = await Matrix.of(context).getLoginClient();
|
||||
await client.importDump(String.fromCharCodes(await file.readAsBytes()));
|
||||
Matrix.of(context).initMatrix();
|
||||
},
|
||||
);
|
||||
}
|
||||
157
lib/pages/intro/intro_page.dart
Normal file
157
lib/pages/intro/intro_page.dart
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/intro/flows/restore_backup_flow.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class IntroPage extends StatelessWidget {
|
||||
const IntroPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final addMultiAccount = Matrix.of(
|
||||
context,
|
||||
).widget.clients.any((client) => client.isLogged());
|
||||
|
||||
return LoginScaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
addMultiAccount
|
||||
? L10n.of(context).addAccount
|
||||
: L10n.of(context).login,
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
useRootNavigator: true,
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
onTap: () => restoreBackupFlow(context),
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
const Icon(Icons.import_export_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).hydrate),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => launchUrl(AppConfig.privacyUrl),
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
const Icon(Icons.privacy_tip_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).privacy),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: () => PlatformInfos.showDialog(context),
|
||||
child: Row(
|
||||
mainAxisSize: .min,
|
||||
children: [
|
||||
const Icon(Icons.info_outlined),
|
||||
const SizedBox(width: 12),
|
||||
Text(L10n.of(context).about),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Hero(
|
||||
tag: 'info-logo',
|
||||
child: Image.asset(
|
||||
'./assets/banner_transparent.png',
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: SelectableLinkify(
|
||||
text: L10n.of(context).appIntro,
|
||||
textScaleFactor: MediaQuery.textScalerOf(
|
||||
context,
|
||||
).scale(1),
|
||||
textAlign: TextAlign.center,
|
||||
linkStyle: TextStyle(
|
||||
color: theme.colorScheme.secondary,
|
||||
decorationColor: theme.colorScheme.secondary,
|
||||
),
|
||||
onOpen: (link) => launchUrlString(link.url),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .stretch,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.secondary,
|
||||
foregroundColor: theme.colorScheme.onSecondary,
|
||||
),
|
||||
onPressed: () => context.go(
|
||||
'${GoRouterState.of(context).uri.path}/sign_up',
|
||||
),
|
||||
child: Text(L10n.of(context).createNewAccount),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go(
|
||||
'${GoRouterState.of(context).uri.path}/sign_in',
|
||||
),
|
||||
child: Text(L10n.of(context).signIn),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final client = await Matrix.of(
|
||||
context,
|
||||
).getLoginClient();
|
||||
context.go(
|
||||
'${GoRouterState.of(context).uri.path}/login',
|
||||
extra: client,
|
||||
);
|
||||
},
|
||||
child: Text(L10n.of(context).loginWithMatrixId),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'login.dart';
|
||||
|
||||
class LoginView extends StatelessWidget {
|
||||
|
|
@ -15,32 +14,18 @@ class LoginView extends StatelessWidget {
|
|||
final theme = Theme.of(context);
|
||||
|
||||
final homeserver = controller.widget.client.homeserver
|
||||
.toString()
|
||||
?.toString()
|
||||
.replaceFirst('https://', '');
|
||||
final title = L10n.of(context).logInTo(homeserver);
|
||||
final titleParts = title.split(homeserver);
|
||||
final title = homeserver == null
|
||||
? L10n.of(context).loginWithMatrixId
|
||||
: L10n.of(context).logInTo(homeserver);
|
||||
|
||||
return LoginScaffold(
|
||||
enforceMobileMode: Matrix.of(
|
||||
context,
|
||||
).widget.clients.any((client) => client.isLogged()),
|
||||
appBar: AppBar(
|
||||
leading: controller.loading ? null : const Center(child: BackButton()),
|
||||
automaticallyImplyLeading: !controller.loading,
|
||||
titleSpacing: !controller.loading ? 0 : null,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(text: titleParts.first),
|
||||
TextSpan(
|
||||
text: homeserver,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: titleParts.last),
|
||||
],
|
||||
),
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
title: Text(title),
|
||||
),
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
|
|
@ -121,18 +106,19 @@ class LoginView extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: TextButton(
|
||||
onPressed: controller.loading
|
||||
? () {}
|
||||
: controller.passwordForgotten,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
if (homeserver != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: TextButton(
|
||||
onPressed: controller.loading
|
||||
? () {}
|
||||
: controller.passwordForgotten,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: theme.colorScheme.error,
|
||||
),
|
||||
child: Text(L10n.of(context).passwordForgotten),
|
||||
),
|
||||
child: Text(L10n.of(context).passwordForgotten),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
215
lib/pages/sign_in/sign_in_page.dart
Normal file
215
lib/pages/sign_in/sign_in_page.dart
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/sign_in/view_model/flows/check_homeserver.dart';
|
||||
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
|
||||
import 'package:fluffychat/pages/sign_in/view_model/sign_in_view_model.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
import 'package:fluffychat/widgets/view_model_builder.dart';
|
||||
|
||||
class SignInPage extends StatelessWidget {
|
||||
final bool signUp;
|
||||
const SignInPage({required this.signUp, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return ViewModelBuilder(
|
||||
create: () => SignInViewModel(Matrix.of(context), signUp: signUp),
|
||||
builder: (context, viewModel, _) {
|
||||
final state = viewModel.value;
|
||||
final publicHomeservers = state.filteredPublicHomeservers;
|
||||
final selectedHomserver = state.selectedHomeserver;
|
||||
return LoginScaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
surfaceTintColor: theme.colorScheme.surface,
|
||||
scrolledUnderElevation: 0,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
signUp
|
||||
? L10n.of(context).createNewAccount
|
||||
: L10n.of(context).login,
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(56 + 60),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .center,
|
||||
spacing: 12,
|
||||
children: [
|
||||
SelectableText(
|
||||
signUp
|
||||
? L10n.of(context).signUpGreeting
|
||||
: L10n.of(context).signInGreeting,
|
||||
textAlign: .center,
|
||||
),
|
||||
TextField(
|
||||
readOnly:
|
||||
state.publicHomeservers.connectionState ==
|
||||
ConnectionState.waiting,
|
||||
controller: viewModel.filterTextController,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: theme.colorScheme.secondaryContainer,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
errorText: state.publicHomeservers.error
|
||||
?.toLocalizedString(context),
|
||||
prefixIcon: const Icon(Icons.search_outlined),
|
||||
hintText: 'Search or enter homeserver address',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: state.publicHomeservers.connectionState == ConnectionState.done
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
child: RadioGroup<PublicHomeserverData>(
|
||||
groupValue: state.selectedHomeserver,
|
||||
onChanged: viewModel.selectHomeserver,
|
||||
child: ListView.builder(
|
||||
itemCount: publicHomeservers.length,
|
||||
itemBuilder: (context, i) {
|
||||
final server = publicHomeservers[i];
|
||||
return RadioListTile.adaptive(
|
||||
value: server,
|
||||
radioScaleFactor: 2,
|
||||
secondary: IconButton(
|
||||
icon: const Icon(Icons.link_outlined),
|
||||
onPressed: () => launchUrlString(
|
||||
server.homepage ?? 'https://${server.name}',
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
Expanded(child: Text(server.name ?? 'Unknown')),
|
||||
...?server.languages?.map(
|
||||
(language) => Material(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
color: theme.colorScheme.tertiaryContainer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6.0,
|
||||
vertical: 3.0,
|
||||
),
|
||||
child: Text(
|
||||
language,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: theme
|
||||
.colorScheme
|
||||
.onTertiaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
spacing: 4.0,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (server.features?.isNotEmpty == true)
|
||||
Row(
|
||||
spacing: 4.0,
|
||||
children: server.features!
|
||||
.map(
|
||||
(feature) => Material(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
color: theme
|
||||
.colorScheme
|
||||
.secondaryContainer,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 6.0,
|
||||
vertical: 3.0,
|
||||
),
|
||||
child: Text(
|
||||
feature,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: theme
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
Text(
|
||||
server.description ?? 'A matrix homeserver',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Center(child: CircularProgressIndicator.adaptive()),
|
||||
bottomNavigationBar: AnimatedSize(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
child:
|
||||
selectedHomserver == null ||
|
||||
!publicHomeservers.contains(selectedHomserver)
|
||||
? const SizedBox.shrink()
|
||||
: Material(
|
||||
elevation: 8,
|
||||
shadowColor: theme.appBarTheme.shadowColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
state.loginLoading.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? null
|
||||
: () => connectToHomeserverFlow(
|
||||
selectedHomserver,
|
||||
context,
|
||||
viewModel.setLoginLoading,
|
||||
signUp,
|
||||
),
|
||||
child:
|
||||
state.loginLoading.connectionState ==
|
||||
ConnectionState.waiting
|
||||
? const CircularProgressIndicator.adaptive()
|
||||
: Text(L10n.of(context).continueText),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
72
lib/pages/sign_in/view_model/flows/check_homeserver.dart
Normal file
72
lib/pages/sign_in/view_model/flows/check_homeserver.dart
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/sign_in/view_model/flows/sso_login.dart';
|
||||
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
void connectToHomeserverFlow(
|
||||
PublicHomeserverData homeserverData,
|
||||
BuildContext context,
|
||||
void Function(AsyncSnapshot<bool>) setState,
|
||||
bool signUp,
|
||||
) async {
|
||||
setState(AsyncSnapshot.waiting());
|
||||
try {
|
||||
final homeserverInput = homeserverData.name!;
|
||||
var homeserver = Uri.parse(homeserverInput);
|
||||
if (homeserver.scheme.isEmpty) {
|
||||
homeserver = Uri.https(homeserverInput, '');
|
||||
}
|
||||
final l10n = L10n.of(context);
|
||||
final client = await Matrix.of(context).getLoginClient();
|
||||
final (_, _, loginFlows, _) = await client.checkHomeserver(homeserver);
|
||||
|
||||
final supportsSso = loginFlows.any((flow) => flow.type == 'm.login.sso');
|
||||
|
||||
if (!supportsSso) {
|
||||
final regLink = homeserverData.regLink;
|
||||
if (signUp && regLink != null) {
|
||||
await launchUrlString(regLink);
|
||||
}
|
||||
|
||||
final pathSegments = List.of(
|
||||
GoRouter.of(context).routeInformationProvider.value.uri.pathSegments,
|
||||
);
|
||||
pathSegments.removeLast();
|
||||
pathSegments.add('login');
|
||||
context.go('/${pathSegments.join('/')}', extra: client);
|
||||
setState(AsyncSnapshot.withData(ConnectionState.done, true));
|
||||
return;
|
||||
}
|
||||
if (kIsWeb || PlatformInfos.isLinux) {
|
||||
final consent = await showOkCancelAlertDialog(
|
||||
context: context,
|
||||
title: l10n.appWantsToUseForLogin(homeserverInput),
|
||||
message: l10n.appWantsToUseForLoginDescription,
|
||||
okLabel: l10n.continueText,
|
||||
);
|
||||
if (consent != OkCancelResult.ok) return;
|
||||
}
|
||||
await ssoLoginFlow(client, context, signUp);
|
||||
|
||||
setState(AsyncSnapshot.withData(ConnectionState.done, true));
|
||||
} catch (e, s) {
|
||||
setState(AsyncSnapshot.withError(ConnectionState.done, e, s));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
e.toLocalizedString(context, ExceptionContext.checkHomeserver),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
24
lib/pages/sign_in/view_model/flows/sort_homeservers.dart
Normal file
24
lib/pages/sign_in/view_model/flows/sort_homeservers.dart
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
|
||||
|
||||
int sortHomeservers(PublicHomeserverData a, PublicHomeserverData b) {
|
||||
return _calcHomeserverScore(b).compareTo(_calcHomeserverScore(a));
|
||||
}
|
||||
|
||||
int _calcHomeserverScore(PublicHomeserverData homeserver) {
|
||||
var score = 0;
|
||||
if (homeserver.description?.isNotEmpty == true) score++;
|
||||
if (homeserver.homepage?.isNotEmpty == true) score++;
|
||||
score += (homeserver.languages?.length ?? 0);
|
||||
score += (homeserver.features?.length ?? 0);
|
||||
score += (homeserver.onlineStatus ?? 0);
|
||||
if (homeserver.ipv6 == true) score++;
|
||||
if (homeserver.isp?.isNotEmpty == true) score++;
|
||||
if (homeserver.privacy?.isNotEmpty == true) score++;
|
||||
if (homeserver.rules?.isNotEmpty == true) score++;
|
||||
if (homeserver.version?.isNotEmpty == true) score++;
|
||||
if (homeserver.usingVanillaReg == true) score--;
|
||||
if (homeserver.regLink != null) score--;
|
||||
if (homeserver.regMethod != 'SSO') score--;
|
||||
if (homeserver.regMethod == 'In-house Element') score--;
|
||||
return score;
|
||||
}
|
||||
49
lib/pages/sign_in/view_model/flows/sso_login.dart
Normal file
49
lib/pages/sign_in/view_model/flows/sso_login.dart
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
||||
Future<void> ssoLoginFlow(
|
||||
Client client,
|
||||
BuildContext context,
|
||||
bool signUp,
|
||||
) async {
|
||||
final redirectUrl = kIsWeb
|
||||
? Uri.parse(
|
||||
html.window.location.href,
|
||||
).resolveUri(Uri(pathSegments: ['auth.html'])).toString()
|
||||
: (PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS)
|
||||
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
|
||||
: 'http://localhost:3001//login';
|
||||
|
||||
final url = client.homeserver!.replace(
|
||||
path: '/_matrix/client/v3/login/sso/redirect',
|
||||
queryParameters: {
|
||||
'redirectUrl': redirectUrl,
|
||||
'action': signUp ? 'register' : 'login',
|
||||
},
|
||||
);
|
||||
|
||||
final urlScheme =
|
||||
(PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS)
|
||||
? Uri.parse(redirectUrl).scheme
|
||||
: "http://localhost:3001";
|
||||
final result = await FlutterWebAuth2.authenticate(
|
||||
url: url.toString(),
|
||||
callbackUrlScheme: urlScheme,
|
||||
options: FlutterWebAuth2Options(useWebview: PlatformInfos.isMobile),
|
||||
);
|
||||
final token = Uri.parse(result).queryParameters['loginToken'];
|
||||
if (token?.isEmpty ?? false) return;
|
||||
|
||||
await client.login(
|
||||
LoginType.mLoginToken,
|
||||
token: token,
|
||||
initialDeviceDisplayName: PlatformInfos.clientName,
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
class PublicHomeserverData {
|
||||
final String? name;
|
||||
final String? clientDomain;
|
||||
final String? homepage;
|
||||
final String? isp;
|
||||
final String? staffJur;
|
||||
final String? rules;
|
||||
final String? privacy;
|
||||
final bool? usingVanillaReg;
|
||||
final String? description;
|
||||
final String? regMethod;
|
||||
final String? regLink;
|
||||
final String? software;
|
||||
final String? version;
|
||||
final bool? captcha;
|
||||
final bool? email;
|
||||
final List<String>? languages;
|
||||
final List<String>? features;
|
||||
final int? onlineStatus;
|
||||
final String? serverDomain;
|
||||
final int? verStatus;
|
||||
final int? roomDirectory;
|
||||
final bool? slidingSync;
|
||||
final bool? ipv6;
|
||||
|
||||
PublicHomeserverData({
|
||||
this.name,
|
||||
this.clientDomain,
|
||||
this.homepage,
|
||||
this.isp,
|
||||
this.staffJur,
|
||||
this.rules,
|
||||
this.privacy,
|
||||
this.usingVanillaReg,
|
||||
this.description,
|
||||
this.regMethod,
|
||||
this.regLink,
|
||||
this.software,
|
||||
this.version,
|
||||
this.captcha,
|
||||
this.email,
|
||||
this.languages,
|
||||
this.features,
|
||||
this.onlineStatus,
|
||||
this.serverDomain,
|
||||
this.verStatus,
|
||||
this.roomDirectory,
|
||||
this.slidingSync,
|
||||
this.ipv6,
|
||||
});
|
||||
|
||||
factory PublicHomeserverData.fromJson(Map<String, dynamic> json) {
|
||||
return PublicHomeserverData(
|
||||
name: json['name'],
|
||||
clientDomain: json['client_domain'],
|
||||
homepage: json['homepage'],
|
||||
isp: json['isp'],
|
||||
staffJur: json['staff_jur'],
|
||||
rules: json['rules'],
|
||||
privacy: json['privacy'],
|
||||
usingVanillaReg: json['using_vanilla_reg'],
|
||||
description: json['description'],
|
||||
regMethod: json['reg_method'],
|
||||
regLink: json['reg_link'],
|
||||
software: json['software'],
|
||||
version: json['version'],
|
||||
captcha: json['captcha'],
|
||||
email: json['email'],
|
||||
languages: List<String>.from(json['languages'] ?? []),
|
||||
features: List<String>.from(json['features'] ?? []),
|
||||
onlineStatus: json['online_status'],
|
||||
serverDomain: json['server_domain'],
|
||||
verStatus: json['ver_status'],
|
||||
roomDirectory: json['room_directory'],
|
||||
slidingSync: json['sliding_sync'],
|
||||
ipv6: json['ipv6'],
|
||||
);
|
||||
}
|
||||
}
|
||||
32
lib/pages/sign_in/view_model/sign_in_state.dart
Normal file
32
lib/pages/sign_in/view_model/sign_in_state.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
|
||||
|
||||
class SignInState {
|
||||
final PublicHomeserverData? selectedHomeserver;
|
||||
final AsyncSnapshot<List<PublicHomeserverData>> publicHomeservers;
|
||||
final List<PublicHomeserverData> filteredPublicHomeservers;
|
||||
final AsyncSnapshot<bool> loginLoading;
|
||||
|
||||
const SignInState({
|
||||
this.selectedHomeserver,
|
||||
this.publicHomeservers = const AsyncSnapshot.nothing(),
|
||||
this.loginLoading = const AsyncSnapshot.nothing(),
|
||||
this.filteredPublicHomeservers = const [],
|
||||
});
|
||||
|
||||
SignInState copyWith({
|
||||
PublicHomeserverData? selectedHomeserver,
|
||||
AsyncSnapshot<List<PublicHomeserverData>>? publicHomeservers,
|
||||
AsyncSnapshot<bool>? loginLoading,
|
||||
List<PublicHomeserverData>? filteredPublicHomeservers,
|
||||
}) {
|
||||
return SignInState(
|
||||
selectedHomeserver: selectedHomeserver ?? this.selectedHomeserver,
|
||||
publicHomeservers: publicHomeservers ?? this.publicHomeservers,
|
||||
loginLoading: loginLoading ?? this.loginLoading,
|
||||
filteredPublicHomeservers:
|
||||
filteredPublicHomeservers ?? this.filteredPublicHomeservers,
|
||||
);
|
||||
}
|
||||
}
|
||||
112
lib/pages/sign_in/view_model/sign_in_view_model.dart
Normal file
112
lib/pages/sign_in/view_model/sign_in_view_model.dart
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:matrix/matrix_api_lite/utils/logs.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/setting_keys.dart';
|
||||
import 'package:fluffychat/pages/sign_in/view_model/flows/sort_homeservers.dart';
|
||||
import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart';
|
||||
import 'package:fluffychat/pages/sign_in/view_model/sign_in_state.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class SignInViewModel extends ValueNotifier<SignInState> {
|
||||
final MatrixState matrixService;
|
||||
final bool signUp;
|
||||
final TextEditingController filterTextController = TextEditingController();
|
||||
|
||||
SignInViewModel(this.matrixService, {required this.signUp})
|
||||
: super(SignInState()) {
|
||||
refreshPublicHomeservers();
|
||||
filterTextController.addListener(_filterHomeservers);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
filterTextController.removeListener(_filterHomeservers);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _filterHomeservers() {
|
||||
final filterText = filterTextController.text.trim().toLowerCase();
|
||||
final filteredPublicHomeservers =
|
||||
value.publicHomeservers.data
|
||||
?.where(
|
||||
(homeserver) =>
|
||||
homeserver.name?.toLowerCase().contains(filterText) ?? false,
|
||||
)
|
||||
.toList() ??
|
||||
[];
|
||||
final splitted = filterText.split('.');
|
||||
if (splitted.length >= 2 && !splitted.any((part) => part.isEmpty)) {
|
||||
if (!filteredPublicHomeservers.any(
|
||||
(homeserver) => homeserver.name == filterText,
|
||||
)) {
|
||||
filteredPublicHomeservers.add(PublicHomeserverData(name: filterText));
|
||||
}
|
||||
}
|
||||
value = value.copyWith(
|
||||
filteredPublicHomeservers: filteredPublicHomeservers,
|
||||
);
|
||||
}
|
||||
|
||||
void refreshPublicHomeservers() async {
|
||||
value = value.copyWith(publicHomeservers: AsyncSnapshot.waiting());
|
||||
final defaultHomeserverData = PublicHomeserverData(
|
||||
name: AppSettings.defaultHomeserver.value,
|
||||
);
|
||||
try {
|
||||
final client = await matrixService.getLoginClient();
|
||||
final response = await client.httpClient.get(AppConfig.homeserverList);
|
||||
final json = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final homeserverJsonList = json['public_servers'] as List;
|
||||
|
||||
final publicHomeservers = homeserverJsonList
|
||||
.map((json) => PublicHomeserverData.fromJson(json))
|
||||
.toList();
|
||||
|
||||
if (signUp) {
|
||||
publicHomeservers.removeWhere((server) {
|
||||
return server.regMethod == null;
|
||||
});
|
||||
}
|
||||
|
||||
publicHomeservers.sort(sortHomeservers);
|
||||
|
||||
final defaultServer =
|
||||
publicHomeservers.singleWhereOrNull(
|
||||
(server) => server.name == AppSettings.defaultHomeserver.value,
|
||||
) ??
|
||||
defaultHomeserverData;
|
||||
|
||||
publicHomeservers.insert(0, defaultServer);
|
||||
|
||||
value = value.copyWith(
|
||||
selectedHomeserver: value.selectedHomeserver ?? publicHomeservers.first,
|
||||
publicHomeservers: AsyncSnapshot.withData(
|
||||
ConnectionState.done,
|
||||
publicHomeservers,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
Logs().w('Unable to fetch public homeservers...', e, s);
|
||||
value = value.copyWith(
|
||||
selectedHomeserver: defaultHomeserverData,
|
||||
publicHomeservers: AsyncSnapshot.withData(ConnectionState.done, [
|
||||
defaultHomeserverData,
|
||||
]),
|
||||
);
|
||||
}
|
||||
_filterHomeservers();
|
||||
}
|
||||
|
||||
void selectHomeserver(PublicHomeserverData? publicHomeserverData) {
|
||||
value = value.copyWith(selectedHomeserver: publicHomeserverData);
|
||||
}
|
||||
|
||||
void setLoginLoading(AsyncSnapshot<bool> loginLoading) {
|
||||
value = value.copyWith(loginLoading: loginLoading);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue