From 3c5855c2d1123086f91ab59c5f3dfa85cff59a7f Mon Sep 17 00:00:00 2001 From: Krille Date: Tue, 29 Oct 2024 08:07:34 +0100 Subject: [PATCH] design: New login design --- assets/l10n/intl_en.arb | 4 +- .../homeserver_picker/homeserver_picker.dart | 39 +- .../homeserver_picker_view.dart | 348 +++++++++++------- lib/pages/login/login_view.dart | 7 +- lib/widgets/layouts/login_scaffold.dart | 11 - 5 files changed, 247 insertions(+), 162 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 40ba16c50..cac5777c5 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2788,5 +2788,7 @@ } }, "oneOfYourDevicesIsNotVerified": "One of your devices is not verified", - "noticeChatBackupDeviceVerification": "Note: When you connect all your devices to the chat backup, they are automatically verified." + "noticeChatBackupDeviceVerification": "Note: When you connect all your devices to the chat backup, they are automatically verified.", + "continueText": "Continue", + "welcomeText": "Hey Hey 👋 This is FluffyChat. You can sign in to any homeserver, which is compatible with https://matrix.org. And then chat with anyone. It's a huge decentralized messaging network!" } diff --git a/lib/pages/homeserver_picker/homeserver_picker.dart b/lib/pages/homeserver_picker/homeserver_picker.dart index 81b33aa00..679b60206 100644 --- a/lib/pages/homeserver_picker/homeserver_picker.dart +++ b/lib/pages/homeserver_picker/homeserver_picker.dart @@ -11,6 +11,7 @@ import 'package:go_router/go_router.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:universal_html/html.dart' as html; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; @@ -83,29 +84,30 @@ class HomeserverPickerController extends State { /// well-known information and forwards to the login page depending on the /// login type. Future checkHomeserverAction([_]) async { - homeserverController.text = + final homeserverInput = homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); - if (homeserverController.text.isEmpty) { + if (homeserverInput.isEmpty || !homeserverInput.contains('.')) { setState(() { error = loginFlows = null; isLoading = false; Matrix.of(context).getLoginClient().homeserver = null; + _lastCheckedUrl = null; }); return; } - if (_lastCheckedUrl == homeserverController.text) return; + if (_lastCheckedUrl == homeserverInput) return; - _lastCheckedUrl = homeserverController.text; + _lastCheckedUrl = homeserverInput; setState(() { error = loginFlows = null; isLoading = true; }); try { - var homeserver = Uri.parse(homeserverController.text); + var homeserver = Uri.parse(homeserverInput); if (homeserver.scheme.isEmpty) { - homeserver = Uri.https(homeserverController.text, ''); + homeserver = Uri.https(homeserverInput, ''); } final client = Matrix.of(context).getLoginClient(); final (_, _, loginFlows) = await client.checkHomeserver(homeserver); @@ -185,9 +187,15 @@ class HomeserverPickerController extends State { } } - void login() => context.push( - '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login', - ); + void login() async { + if (!supportsPasswordLogin) { + homeserverController.text = AppConfig.defaultHomeserver; + await checkHomeserverAction(); + } + context.push( + '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login', + ); + } @override void initState() { @@ -223,8 +231,21 @@ class HomeserverPickerController extends State { } } } + + void onMoreAction(MoreLoginActions action) { + switch (action) { + case MoreLoginActions.passwordLogin: + login(); + case MoreLoginActions.privacy: + launchUrlString(AppConfig.privacyUrl); + case MoreLoginActions.about: + PlatformInfos.showDialog(context); + } + } } +enum MoreLoginActions { passwordLogin, privacy, about } + class IdentityProvider { final String? id; final String? name; diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 4aa4d2929..4cd02cdaa 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.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/widgets/adaptive_dialog_action.dart'; @@ -22,159 +23,228 @@ class HomeserverPickerView extends StatelessWidget { return LoginScaffold( enforceMobileMode: Matrix.of(context).client.isLogged(), - appBar: controller.widget.addMultiAccount - ? AppBar( - centerTitle: true, - title: Text(L10n.of(context).addAccount), - ) - : null, - body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // display a prominent banner to import session for TOR browser - // users. This feature is just some UX sugar as TOR users are - // usually forced to logout as TOR browser is non-persistent - AnimatedContainer( - height: controller.isTorBrowser ? 64 : 0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: Material( - clipBehavior: Clip.hardEdge, - borderRadius: - const BorderRadius.vertical(bottom: Radius.circular(8)), - color: theme.colorScheme.surface, - child: ListTile( - leading: const Icon(Icons.vpn_key), - title: Text(L10n.of(context).hydrateTor), - subtitle: Text(L10n.of(context).hydrateTorLong), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: controller.restoreBackup, + appBar: AppBar( + centerTitle: true, + title: Text(L10n.of(context).addAccount), + actions: [ + PopupMenuButton( + onSelected: controller.onMoreAction, + itemBuilder: (_) => [ + PopupMenuItem( + value: MoreLoginActions.passwordLogin, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.login_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).loginWithMatrixId), + ], + ), ), - ), - ), - if (MediaQuery.of(context).size.height > 512) - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height / 4, + PopupMenuItem( + value: MoreLoginActions.privacy, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.privacy_tip_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).privacy), + ], + ), ), - child: Image.asset( - 'assets/banner_transparent.png', - alignment: Alignment.center, - repeat: ImageRepeat.repeat, + PopupMenuItem( + value: MoreLoginActions.about, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outlined), + const SizedBox(width: 12), + Text(L10n.of(context).about), + ], + ), ), - ), - Padding( - padding: const EdgeInsets.all(32.0), - child: TextField( - onChanged: controller.tryCheckHomeserverActionWithCooldown, - onEditingComplete: - controller.tryCheckHomeserverActionWithoutCooldown, - onSubmitted: controller.tryCheckHomeserverActionWithoutCooldown, - onTap: controller.tryCheckHomeserverActionWithCooldown, - controller: controller.homeserverController, - autocorrect: false, - keyboardType: TextInputType.url, - decoration: InputDecoration( - prefixIcon: controller.isLoading - ? Container( - width: 16, - height: 16, - alignment: Alignment.center, - child: const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), + ], + ), + ], + ), + body: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: Column( + children: [ + // display a prominent banner to import session for TOR browser + // users. This feature is just some UX sugar as TOR users are + // usually forced to logout as TOR browser is non-persistent + AnimatedContainer( + height: controller.isTorBrowser ? 64 : 0, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: Material( + clipBehavior: Clip.hardEdge, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(8), ), - ) - : const Icon(Icons.search_outlined), - filled: false, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - hintText: AppConfig.defaultHomeserver, - labelText: L10n.of(context).homeserver, - errorText: controller.error, - suffixIcon: IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => AlertDialog.adaptive( - title: Text(L10n.of(context).whatIsAHomeserver), - content: Linkify( - text: L10n.of(context).homeserverDescription, + color: theme.colorScheme.surface, + child: ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(L10n.of(context).hydrateTor), + subtitle: Text(L10n.of(context).hydrateTorLong), + trailing: const Icon(Icons.chevron_right_outlined), + onTap: controller.restoreBackup, ), - actions: [ - AdaptiveDialogAction( - onPressed: () => launchUrl( - Uri.https('servers.joinmatrix.org'), + ), + ), + 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).welcomeText, + style: TextStyle( + color: theme.colorScheme.onSecondaryContainer, + fontWeight: FontWeight.w500, + ), + 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: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + onChanged: + controller.tryCheckHomeserverActionWithCooldown, + onEditingComplete: controller + .tryCheckHomeserverActionWithoutCooldown, + onSubmitted: controller + .tryCheckHomeserverActionWithoutCooldown, + onTap: + controller.tryCheckHomeserverActionWithCooldown, + controller: controller.homeserverController, + autocorrect: false, + keyboardType: TextInputType.url, + decoration: InputDecoration( + prefixIcon: controller.isLoading + ? Container( + width: 16, + height: 16, + alignment: Alignment.center, + child: const SizedBox( + width: 16, + height: 16, + child: + CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ) + : const Icon(Icons.search_outlined), + filled: false, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ), + hintText: AppConfig.defaultHomeserver, + hintStyle: TextStyle( + color: theme.colorScheme.surfaceTint, + ), + labelText: 'Sign in with:', + 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, + ), + 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), + ), ), - child: Text( - L10n.of(context).discoverHomeservers, + ), + const SizedBox(height: 32), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, ), + onPressed: + controller.isLoggingIn || controller.isLoading + ? null + : controller.supportsSso + ? controller.ssoLoginAction + : controller.supportsPasswordLogin + ? controller.login + : null, + child: Text(L10n.of(context).continueText), ), - AdaptiveDialogAction( - onPressed: Navigator.of(context).pop, - child: Text(L10n.of(context).close), + TextButton( + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.secondary, + textStyle: theme.textTheme.labelMedium, + ), + onPressed: + controller.isLoggingIn || controller.isLoading + ? null + : controller.restoreBackup, + child: Text(L10n.of(context).hydrate), ), ], ), - ); - }, - icon: const Icon(Icons.info_outlined), + ), + ], ), ), ), - ), - if (MediaQuery.of(context).size.height > 512) const Spacer(), - ListView( - shrinkWrap: true, - padding: const EdgeInsets.symmetric( - horizontal: 32.0, - vertical: 32.0, - ), - children: [ - TextButton( - style: TextButton.styleFrom( - textStyle: theme.textTheme.labelMedium, - foregroundColor: theme.colorScheme.secondary, - ), - onPressed: controller.isLoggingIn || controller.isLoading - ? null - : controller.restoreBackup, - child: Text(L10n.of(context).hydrate), - ), - if (controller.supportsPasswordLogin && controller.supportsSso) - TextButton( - style: TextButton.styleFrom( - foregroundColor: theme.colorScheme.secondary, - textStyle: theme.textTheme.labelMedium, - ), - onPressed: controller.isLoggingIn || controller.isLoading - ? null - : controller.login, - child: Text(L10n.of(context).loginWithMatrixId), - ), - const SizedBox(height: 8.0), - if (controller.supportsPasswordLogin || controller.supportsSso) - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - foregroundColor: theme.colorScheme.onPrimary, - ), - onPressed: controller.isLoggingIn || controller.isLoading - ? null - : controller.supportsSso - ? controller.ssoLoginAction - : controller.login, - child: Text(L10n.of(context).next), - ), - ], - ), - ], + ); + }, ), ); } diff --git a/lib/pages/login/login_view.dart b/lib/pages/login/login_view.dart index 7a7dbc953..5b04bbad0 100644 --- a/lib/pages/login/login_view.dart +++ b/lib/pages/login/login_view.dart @@ -49,7 +49,10 @@ class LoginView extends StatelessWidget { child: ListView( padding: const EdgeInsets.symmetric(horizontal: 8), children: [ - Image.asset('assets/banner_transparent.png'), + Hero( + tag: 'info-logo', + child: Image.asset('assets/banner_transparent.png'), + ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), @@ -67,7 +70,7 @@ class LoginView extends StatelessWidget { prefixIcon: const Icon(Icons.account_box_outlined), errorText: controller.usernameError, errorStyle: const TextStyle(color: Colors.orange), - hintText: '@username:localpart', + hintText: '@username:domain', labelText: L10n.of(context).emailOrUsername, ), ), diff --git a/lib/widgets/layouts/login_scaffold.dart b/lib/widgets/layouts/login_scaffold.dart index d2363ced4..354cf4125 100644 --- a/lib/widgets/layouts/login_scaffold.dart +++ b/lib/widgets/layouts/login_scaffold.dart @@ -44,17 +44,6 @@ class LoginScaffold extends StatelessWidget { body: SafeArea(child: body), backgroundColor: isMobileMode ? null : theme.colorScheme.surface.withOpacity(0.8), - bottomNavigationBar: isMobileMode - ? Material( - elevation: 4, - shadowColor: theme.colorScheme.onSurface, - child: const SafeArea( - child: _PrivacyButtons( - mainAxisAlignment: MainAxisAlignment.center, - ), - ), - ) - : null, ); if (isMobileMode) return scaffold; return Container(