design: New login design

pull/1439/head
Krille 9 months ago
parent a18e2c4db7
commit 3c5855c2d1
No known key found for this signature in database
GPG Key ID: E067ECD60F1A0652

@ -2788,5 +2788,7 @@
} }
}, },
"oneOfYourDevicesIsNotVerified": "One of your devices is not verified", "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!"
} }

@ -11,6 +11,7 @@ import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:universal_html/html.dart' as html; 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/config/app_config.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
@ -83,29 +84,30 @@ class HomeserverPickerController extends State<HomeserverPicker> {
/// well-known information and forwards to the login page depending on the /// well-known information and forwards to the login page depending on the
/// login type. /// login type.
Future<void> checkHomeserverAction([_]) async { Future<void> checkHomeserverAction([_]) async {
homeserverController.text = final homeserverInput =
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); homeserverController.text.trim().toLowerCase().replaceAll(' ', '-');
if (homeserverController.text.isEmpty) { if (homeserverInput.isEmpty || !homeserverInput.contains('.')) {
setState(() { setState(() {
error = loginFlows = null; error = loginFlows = null;
isLoading = false; isLoading = false;
Matrix.of(context).getLoginClient().homeserver = null; Matrix.of(context).getLoginClient().homeserver = null;
_lastCheckedUrl = null;
}); });
return; return;
} }
if (_lastCheckedUrl == homeserverController.text) return; if (_lastCheckedUrl == homeserverInput) return;
_lastCheckedUrl = homeserverController.text; _lastCheckedUrl = homeserverInput;
setState(() { setState(() {
error = loginFlows = null; error = loginFlows = null;
isLoading = true; isLoading = true;
}); });
try { try {
var homeserver = Uri.parse(homeserverController.text); var homeserver = Uri.parse(homeserverInput);
if (homeserver.scheme.isEmpty) { if (homeserver.scheme.isEmpty) {
homeserver = Uri.https(homeserverController.text, ''); homeserver = Uri.https(homeserverInput, '');
} }
final client = Matrix.of(context).getLoginClient(); final client = Matrix.of(context).getLoginClient();
final (_, _, loginFlows) = await client.checkHomeserver(homeserver); final (_, _, loginFlows) = await client.checkHomeserver(homeserver);
@ -185,9 +187,15 @@ class HomeserverPickerController extends State<HomeserverPicker> {
} }
} }
void login() => context.push( void login() async {
if (!supportsPasswordLogin) {
homeserverController.text = AppConfig.defaultHomeserver;
await checkHomeserverAction();
}
context.push(
'${GoRouter.of(context).routeInformationProvider.value.uri.path}/login', '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login',
); );
}
@override @override
void initState() { void initState() {
@ -223,8 +231,21 @@ class HomeserverPickerController extends State<HomeserverPicker> {
} }
} }
} }
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 { class IdentityProvider {
final String? id; final String? id;
final String? name; final String? name;

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.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/app_config.dart';
import 'package:fluffychat/widgets/adaptive_dialog_action.dart'; import 'package:fluffychat/widgets/adaptive_dialog_action.dart';
@ -22,14 +23,57 @@ class HomeserverPickerView extends StatelessWidget {
return LoginScaffold( return LoginScaffold(
enforceMobileMode: Matrix.of(context).client.isLogged(), enforceMobileMode: Matrix.of(context).client.isLogged(),
appBar: controller.widget.addMultiAccount appBar: AppBar(
? AppBar(
centerTitle: true, centerTitle: true,
title: Text(L10n.of(context).addAccount), title: Text(L10n.of(context).addAccount),
) actions: [
: null, PopupMenuButton<MoreLoginActions>(
body: Column( onSelected: controller.onMoreAction,
crossAxisAlignment: CrossAxisAlignment.stretch, 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),
],
),
),
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),
],
),
),
PopupMenuItem(
value: MoreLoginActions.about,
child: Row(
mainAxisSize: 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: [ children: [
// display a prominent banner to import session for TOR browser // display a prominent banner to import session for TOR browser
// users. This feature is just some UX sugar as TOR users are // users. This feature is just some UX sugar as TOR users are
@ -42,8 +86,9 @@ class HomeserverPickerView extends StatelessWidget {
decoration: const BoxDecoration(), decoration: const BoxDecoration(),
child: Material( child: Material(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
borderRadius: borderRadius: const BorderRadius.vertical(
const BorderRadius.vertical(bottom: Radius.circular(8)), bottom: Radius.circular(8),
),
color: theme.colorScheme.surface, color: theme.colorScheme.surface,
child: ListTile( child: ListTile(
leading: const Icon(Icons.vpn_key), leading: const Icon(Icons.vpn_key),
@ -54,25 +99,50 @@ class HomeserverPickerView extends StatelessWidget {
), ),
), ),
), ),
if (MediaQuery.of(context).size.height > 512) Container(
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height / 4,
),
child: Image.asset(
'assets/banner_transparent.png',
alignment: Alignment.center, alignment: Alignment.center,
repeat: ImageRepeat.repeat, 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(
padding: const EdgeInsets.all(32.0), padding: const EdgeInsets.all(32.0),
child: TextField( child: Column(
onChanged: controller.tryCheckHomeserverActionWithCooldown, mainAxisSize: MainAxisSize.min,
onEditingComplete: crossAxisAlignment: CrossAxisAlignment.stretch,
controller.tryCheckHomeserverActionWithoutCooldown, children: [
onSubmitted: controller.tryCheckHomeserverActionWithoutCooldown, TextField(
onTap: controller.tryCheckHomeserverActionWithCooldown, onChanged:
controller.tryCheckHomeserverActionWithCooldown,
onEditingComplete: controller
.tryCheckHomeserverActionWithoutCooldown,
onSubmitted: controller
.tryCheckHomeserverActionWithoutCooldown,
onTap:
controller.tryCheckHomeserverActionWithCooldown,
controller: controller.homeserverController, controller: controller.homeserverController,
autocorrect: false, autocorrect: false,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
@ -85,7 +155,8 @@ class HomeserverPickerView extends StatelessWidget {
child: const SizedBox( child: const SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: CircularProgressIndicator.adaptive( child:
CircularProgressIndicator.adaptive(
strokeWidth: 2, strokeWidth: 2,
), ),
), ),
@ -93,19 +164,28 @@ class HomeserverPickerView extends StatelessWidget {
: const Icon(Icons.search_outlined), : const Icon(Icons.search_outlined),
filled: false, filled: false,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius), borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
), ),
hintText: AppConfig.defaultHomeserver, hintText: AppConfig.defaultHomeserver,
labelText: L10n.of(context).homeserver, hintStyle: TextStyle(
color: theme.colorScheme.surfaceTint,
),
labelText: 'Sign in with:',
errorText: controller.error, errorText: controller.error,
errorMaxLines: 4,
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog.adaptive( builder: (context) => AlertDialog.adaptive(
title: Text(L10n.of(context).whatIsAHomeserver), title: Text(
L10n.of(context).whatIsAHomeserver,
),
content: Linkify( content: Linkify(
text: L10n.of(context).homeserverDescription, text: L10n.of(context)
.homeserverDescription,
), ),
actions: [ actions: [
AdaptiveDialogAction( AdaptiveDialogAction(
@ -113,7 +193,8 @@ class HomeserverPickerView extends StatelessWidget {
Uri.https('servers.joinmatrix.org'), Uri.https('servers.joinmatrix.org'),
), ),
child: Text( child: Text(
L10n.of(context).discoverHomeservers, L10n.of(context)
.discoverHomeservers,
), ),
), ),
AdaptiveDialogAction( AdaptiveDialogAction(
@ -128,53 +209,42 @@ class HomeserverPickerView extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(height: 32),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
), ),
if (MediaQuery.of(context).size.height > 512) const Spacer(), onPressed:
ListView( controller.isLoggingIn || controller.isLoading
shrinkWrap: true, ? null
padding: const EdgeInsets.symmetric( : controller.supportsSso
horizontal: 32.0, ? controller.ssoLoginAction
vertical: 32.0, : controller.supportsPasswordLogin
? controller.login
: null,
child: Text(L10n.of(context).continueText),
), ),
children: [
TextButton( TextButton(
style: TextButton.styleFrom( style: TextButton.styleFrom(
textStyle: theme.textTheme.labelMedium,
foregroundColor: theme.colorScheme.secondary, foregroundColor: theme.colorScheme.secondary,
textStyle: theme.textTheme.labelMedium,
), ),
onPressed: controller.isLoggingIn || controller.isLoading onPressed:
controller.isLoggingIn || controller.isLoading
? null ? null
: controller.restoreBackup, : controller.restoreBackup,
child: Text(L10n.of(context).hydrate), 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),
), ),
],
), ),
], );
},
), ),
); );
} }

@ -49,7 +49,10 @@ class LoginView extends StatelessWidget {
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
children: <Widget>[ children: <Widget>[
Image.asset('assets/banner_transparent.png'), Hero(
tag: 'info-logo',
child: Image.asset('assets/banner_transparent.png'),
),
const SizedBox(height: 16), const SizedBox(height: 16),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
@ -67,7 +70,7 @@ class LoginView extends StatelessWidget {
prefixIcon: const Icon(Icons.account_box_outlined), prefixIcon: const Icon(Icons.account_box_outlined),
errorText: controller.usernameError, errorText: controller.usernameError,
errorStyle: const TextStyle(color: Colors.orange), errorStyle: const TextStyle(color: Colors.orange),
hintText: '@username:localpart', hintText: '@username:domain',
labelText: L10n.of(context).emailOrUsername, labelText: L10n.of(context).emailOrUsername,
), ),
), ),

@ -44,17 +44,6 @@ class LoginScaffold extends StatelessWidget {
body: SafeArea(child: body), body: SafeArea(child: body),
backgroundColor: backgroundColor:
isMobileMode ? null : theme.colorScheme.surface.withOpacity(0.8), 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; if (isMobileMode) return scaffold;
return Container( return Container(

Loading…
Cancel
Save