mirror of
https://github.com/krille-chan/fluffychat
synced 2024-08-11 11:13:54 +00:00
feat: New change password page with server capabilities check
This commit is contained in:
parent
3eaecf0783
commit
5847fe0546
7 changed files with 296 additions and 94 deletions
|
@ -2380,5 +2380,10 @@
|
|||
"databaseMigrationBody": "Please wait. This may take a moment.",
|
||||
"leaveEmptyToClearStatus": "Leave empty to clear your status.",
|
||||
"select": "Select",
|
||||
"searchForUsers": "Search for @users..."
|
||||
"searchForUsers": "Search for @users...",
|
||||
"pleaseEnterYourCurrentPassword": "Please enter your current password",
|
||||
"newPassword": "New password",
|
||||
"pleaseChooseAStrongPassword": "Please choose a strong password",
|
||||
"passwordsDoNotMatch": "Passwords do not match",
|
||||
"passwordIsWrong": "Your entered password is wrong"
|
||||
}
|
|
@ -26,6 +26,7 @@ import 'package:fluffychat/pages/settings_emotes/settings_emotes.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';
|
||||
import 'package:fluffychat/pages/settings_password/settings_password.dart';
|
||||
import 'package:fluffychat/pages/settings_security/settings_security.dart';
|
||||
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
||||
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
||||
|
@ -241,6 +242,16 @@ abstract class AppRoutes {
|
|||
const SettingsSecurity(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'password',
|
||||
pageBuilder: (context, state) {
|
||||
return defaultPageBuilder(
|
||||
context,
|
||||
const SettingsPassword(),
|
||||
);
|
||||
},
|
||||
redirect: loggedOutRedirect,
|
||||
),
|
||||
GoRoute(
|
||||
path: 'ignorelist',
|
||||
pageBuilder: (context, state) {
|
||||
|
|
83
lib/pages/settings_password/settings_password.dart
Normal file
83
lib/pages/settings_password/settings_password.dart
Normal file
|
@ -0,0 +1,83 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/pages/settings_password/settings_password_view.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
class SettingsPassword extends StatefulWidget {
|
||||
const SettingsPassword({super.key});
|
||||
|
||||
@override
|
||||
SettingsPasswordController createState() => SettingsPasswordController();
|
||||
}
|
||||
|
||||
class SettingsPasswordController extends State<SettingsPassword> {
|
||||
final TextEditingController oldPasswordController = TextEditingController();
|
||||
final TextEditingController newPassword1Controller = TextEditingController();
|
||||
final TextEditingController newPassword2Controller = TextEditingController();
|
||||
|
||||
String? oldPasswordError;
|
||||
String? newPassword1Error;
|
||||
String? newPassword2Error;
|
||||
|
||||
bool loading = false;
|
||||
|
||||
void changePassword() async {
|
||||
setState(() {
|
||||
oldPasswordError = newPassword1Error = newPassword2Error = null;
|
||||
});
|
||||
if (oldPasswordController.text.isEmpty) {
|
||||
setState(() {
|
||||
oldPasswordError = L10n.of(context)!.pleaseEnterYourPassword;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (newPassword1Controller.text.isEmpty ||
|
||||
newPassword1Controller.text.length < 6) {
|
||||
setState(() {
|
||||
newPassword1Error = L10n.of(context)!.pleaseChooseAStrongPassword;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (newPassword1Controller.text != newPassword2Controller.text) {
|
||||
setState(() {
|
||||
newPassword2Error = L10n.of(context)!.passwordsDoNotMatch;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
loading = true;
|
||||
});
|
||||
try {
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
await Matrix.of(context).client.changePassword(
|
||||
newPassword1Controller.text,
|
||||
oldPassword: oldPasswordController.text,
|
||||
);
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(L10n.of(context)!.passwordHasBeenChanged),
|
||||
),
|
||||
);
|
||||
if (mounted) context.pop();
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
newPassword2Error = e.toLocalizedString(
|
||||
context,
|
||||
ExceptionContext.changePassword,
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SettingsPasswordView(this);
|
||||
}
|
75
lib/pages/settings_password/settings_password_view.dart
Normal file
75
lib/pages/settings_password/settings_password_view.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
|
||||
import 'package:fluffychat/pages/settings_password/settings_password.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
|
||||
class SettingsPasswordView extends StatelessWidget {
|
||||
final SettingsPasswordController controller;
|
||||
const SettingsPasswordView(this.controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(L10n.of(context)!.changePassword)),
|
||||
body: ListTileTheme(
|
||||
iconColor: Theme.of(context).colorScheme.onBackground,
|
||||
child: MaxWidthBody(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller.oldPasswordController,
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
autofocus: true,
|
||||
readOnly: controller.loading,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!.pleaseEnterYourCurrentPassword,
|
||||
errorText: controller.oldPasswordError,
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
TextField(
|
||||
controller: controller.newPassword1Controller,
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
readOnly: controller.loading,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!.newPassword,
|
||||
errorText: controller.newPassword1Error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: controller.newPassword2Controller,
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
readOnly: controller.loading,
|
||||
decoration: InputDecoration(
|
||||
hintText: L10n.of(context)!.repeatPassword,
|
||||
errorText: controller.newPassword2Error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed:
|
||||
controller.loading ? null : controller.changePassword,
|
||||
icon: const Icon(Icons.send_outlined),
|
||||
label: controller.loading
|
||||
? const LinearProgressIndicator()
|
||||
: Text(L10n.of(context)!.changePassword),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,42 +23,6 @@ class SettingsSecurity extends StatefulWidget {
|
|||
}
|
||||
|
||||
class SettingsSecurityController extends State<SettingsSecurity> {
|
||||
void changePasswordAccountAction() async {
|
||||
final input = await showTextInputDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
title: L10n.of(context)!.changePassword,
|
||||
okLabel: L10n.of(context)!.ok,
|
||||
cancelLabel: L10n.of(context)!.cancel,
|
||||
textFields: [
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context)!.chooseAStrongPassword,
|
||||
obscureText: true,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
),
|
||||
DialogTextField(
|
||||
hintText: L10n.of(context)!.repeatPassword,
|
||||
obscureText: true,
|
||||
minLines: 1,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
);
|
||||
if (input == null) return;
|
||||
final success = await showFutureLoadingDialog(
|
||||
context: context,
|
||||
future: () => Matrix.of(context)
|
||||
.client
|
||||
.changePassword(input.last, oldPassword: input.first),
|
||||
);
|
||||
if (success.error == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setAppLockAction() async {
|
||||
if (AppLock.of(context).isActive) {
|
||||
AppLock.of(context).showLockScreen();
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'package:fluffychat/utils/beautify_string_extension.dart';
|
||||
import 'package:fluffychat/utils/localized_exception_extension.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
@ -20,66 +21,119 @@ class SettingsSecurityView extends StatelessWidget {
|
|||
body: ListTileTheme(
|
||||
iconColor: Theme.of(context).colorScheme.onBackground,
|
||||
child: MaxWidthBody(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.block_outlined),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(L10n.of(context)!.blockedUsers),
|
||||
onTap: () => context.go('/rooms/settings/security/ignorelist'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.password_outlined),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(
|
||||
L10n.of(context)!.changePassword,
|
||||
),
|
||||
onTap: controller.changePasswordAccountAction,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.mail_outlined),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(L10n.of(context)!.passwordRecovery),
|
||||
onTap: () => context.go('/rooms/settings/security/3pid'),
|
||||
),
|
||||
if (Matrix.of(context).client.encryption != null) ...{
|
||||
const Divider(thickness: 1),
|
||||
if (PlatformInfos.isMobile)
|
||||
child: FutureBuilder(
|
||||
future: Matrix.of(context)
|
||||
.client
|
||||
.getCapabilities()
|
||||
.timeout(const Duration(seconds: 10)),
|
||||
builder: (context, snapshot) {
|
||||
final capabilities = snapshot.data;
|
||||
final error = snapshot.error;
|
||||
if (error == null && capabilities == null) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
if (error != null)
|
||||
ListTile(
|
||||
leading: const Icon(
|
||||
Icons.warning_outlined,
|
||||
color: Colors.orange,
|
||||
),
|
||||
title: Text(
|
||||
error.toLocalizedString(context),
|
||||
style: const TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
if (capabilities?.mChangePassword?.enabled == true ||
|
||||
error != null) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.password_outlined),
|
||||
trailing: error != null
|
||||
? null
|
||||
: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(
|
||||
L10n.of(context)!.changePassword,
|
||||
style: TextStyle(
|
||||
decoration:
|
||||
error == null ? null : TextDecoration.lineThrough,
|
||||
),
|
||||
),
|
||||
onTap: error != null
|
||||
? null
|
||||
: () =>
|
||||
context.go('/rooms/settings/security/password'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.mail_outlined),
|
||||
trailing: error != null
|
||||
? null
|
||||
: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(
|
||||
L10n.of(context)!.passwordRecovery,
|
||||
style: TextStyle(
|
||||
decoration:
|
||||
error == null ? null : TextDecoration.lineThrough,
|
||||
),
|
||||
),
|
||||
onTap: error != null
|
||||
? null
|
||||
: () => context.go('/rooms/settings/security/3pid'),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
ListTile(
|
||||
leading: const Icon(Icons.lock_outlined),
|
||||
leading: const Icon(Icons.block_outlined),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(L10n.of(context)!.appLock),
|
||||
onTap: controller.setAppLockAction,
|
||||
title: Text(L10n.of(context)!.blockedUsers),
|
||||
onTap: () =>
|
||||
context.go('/rooms/settings/security/ignorelist'),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.yourPublicKey),
|
||||
subtitle: Text(
|
||||
Matrix.of(context).client.fingerprintKey.beautified,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
if (Matrix.of(context).client.encryption != null) ...{
|
||||
const Divider(thickness: 1),
|
||||
if (PlatformInfos.isMobile)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.lock_outlined),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(L10n.of(context)!.appLock),
|
||||
onTap: controller.setAppLockAction,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(L10n.of(context)!.yourPublicKey),
|
||||
subtitle: Text(
|
||||
Matrix.of(context).client.fingerprintKey.beautified,
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
),
|
||||
leading: const Icon(Icons.vpn_key_outlined),
|
||||
),
|
||||
},
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.tap_and_play),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(
|
||||
L10n.of(context)!.dehydrate,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
onTap: controller.dehydrateAction,
|
||||
),
|
||||
leading: const Icon(Icons.vpn_key_outlined),
|
||||
),
|
||||
},
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.tap_and_play),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(
|
||||
L10n.of(context)!.dehydrate,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
onTap: controller.dehydrateAction,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outlined),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(
|
||||
L10n.of(context)!.deleteAccount,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
onTap: controller.deleteAccountAction,
|
||||
),
|
||||
],
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outlined),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
title: Text(
|
||||
L10n.of(context)!.deleteAccount,
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
onTap: controller.deleteAccountAction,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -9,10 +9,16 @@ import 'package:matrix/matrix.dart';
|
|||
import 'uia_request_manager.dart';
|
||||
|
||||
extension LocalizedExceptionExtension on Object {
|
||||
String toLocalizedString(BuildContext context) {
|
||||
String toLocalizedString(
|
||||
BuildContext context, [
|
||||
ExceptionContext? exceptionContext,
|
||||
]) {
|
||||
if (this is MatrixException) {
|
||||
switch ((this as MatrixException).error) {
|
||||
case MatrixError.M_FORBIDDEN:
|
||||
if (exceptionContext == ExceptionContext.changePassword) {
|
||||
return L10n.of(context)!.passwordIsWrong;
|
||||
}
|
||||
return L10n.of(context)!.noPermission;
|
||||
case MatrixError.M_LIMIT_EXCEEDED:
|
||||
return L10n.of(context)!.tooManyRequestsWarning;
|
||||
|
@ -70,3 +76,7 @@ extension LocalizedExceptionExtension on Object {
|
|||
return L10n.of(context)!.oopsSomethingWentWrong;
|
||||
}
|
||||
}
|
||||
|
||||
enum ExceptionContext {
|
||||
changePassword,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue