mirror of
https://github.com/krille-chan/fluffychat
synced 2024-09-17 09:35:12 +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.",
|
"databaseMigrationBody": "Please wait. This may take a moment.",
|
||||||
"leaveEmptyToClearStatus": "Leave empty to clear your status.",
|
"leaveEmptyToClearStatus": "Leave empty to clear your status.",
|
||||||
"select": "Select",
|
"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_ignore_list/settings_ignore_list.dart';
|
||||||
import 'package:fluffychat/pages/settings_multiple_emotes/settings_multiple_emotes.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_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_security/settings_security.dart';
|
||||||
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
import 'package:fluffychat/pages/settings_style/settings_style.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
import 'package:fluffychat/widgets/layouts/empty_page.dart';
|
||||||
|
@ -241,6 +242,16 @@ abstract class AppRoutes {
|
||||||
const SettingsSecurity(),
|
const SettingsSecurity(),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'password',
|
||||||
|
pageBuilder: (context, state) {
|
||||||
|
return defaultPageBuilder(
|
||||||
|
context,
|
||||||
|
const SettingsPassword(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
redirect: loggedOutRedirect,
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'ignorelist',
|
path: 'ignorelist',
|
||||||
pageBuilder: (context, state) {
|
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> {
|
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 {
|
void setAppLockAction() async {
|
||||||
if (AppLock.of(context).isActive) {
|
if (AppLock.of(context).isActive) {
|
||||||
AppLock.of(context).showLockScreen();
|
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:go_router/go_router.dart';
|
||||||
|
|
||||||
import 'package:fluffychat/utils/beautify_string_extension.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/utils/platform_infos.dart';
|
||||||
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||||
import 'package:fluffychat/widgets/matrix.dart';
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
@ -20,66 +21,119 @@ class SettingsSecurityView extends StatelessWidget {
|
||||||
body: ListTileTheme(
|
body: ListTileTheme(
|
||||||
iconColor: Theme.of(context).colorScheme.onBackground,
|
iconColor: Theme.of(context).colorScheme.onBackground,
|
||||||
child: MaxWidthBody(
|
child: MaxWidthBody(
|
||||||
child: Column(
|
child: FutureBuilder(
|
||||||
children: [
|
future: Matrix.of(context)
|
||||||
ListTile(
|
.client
|
||||||
leading: const Icon(Icons.block_outlined),
|
.getCapabilities()
|
||||||
trailing: const Icon(Icons.chevron_right_outlined),
|
.timeout(const Duration(seconds: 10)),
|
||||||
title: Text(L10n.of(context)!.blockedUsers),
|
builder: (context, snapshot) {
|
||||||
onTap: () => context.go('/rooms/settings/security/ignorelist'),
|
final capabilities = snapshot.data;
|
||||||
),
|
final error = snapshot.error;
|
||||||
ListTile(
|
if (error == null && capabilities == null) {
|
||||||
leading: const Icon(Icons.password_outlined),
|
return const Center(
|
||||||
trailing: const Icon(Icons.chevron_right_outlined),
|
child: Padding(
|
||||||
title: Text(
|
padding: EdgeInsets.all(16.0),
|
||||||
L10n.of(context)!.changePassword,
|
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
|
||||||
),
|
),
|
||||||
onTap: controller.changePasswordAccountAction,
|
);
|
||||||
),
|
}
|
||||||
ListTile(
|
return Column(
|
||||||
leading: const Icon(Icons.mail_outlined),
|
children: [
|
||||||
trailing: const Icon(Icons.chevron_right_outlined),
|
if (error != null)
|
||||||
title: Text(L10n.of(context)!.passwordRecovery),
|
ListTile(
|
||||||
onTap: () => context.go('/rooms/settings/security/3pid'),
|
leading: const Icon(
|
||||||
),
|
Icons.warning_outlined,
|
||||||
if (Matrix.of(context).client.encryption != null) ...{
|
color: Colors.orange,
|
||||||
const Divider(thickness: 1),
|
),
|
||||||
if (PlatformInfos.isMobile)
|
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(
|
ListTile(
|
||||||
leading: const Icon(Icons.lock_outlined),
|
leading: const Icon(Icons.block_outlined),
|
||||||
trailing: const Icon(Icons.chevron_right_outlined),
|
trailing: const Icon(Icons.chevron_right_outlined),
|
||||||
title: Text(L10n.of(context)!.appLock),
|
title: Text(L10n.of(context)!.blockedUsers),
|
||||||
onTap: controller.setAppLockAction,
|
onTap: () =>
|
||||||
|
context.go('/rooms/settings/security/ignorelist'),
|
||||||
),
|
),
|
||||||
ListTile(
|
if (Matrix.of(context).client.encryption != null) ...{
|
||||||
title: Text(L10n.of(context)!.yourPublicKey),
|
const Divider(thickness: 1),
|
||||||
subtitle: Text(
|
if (PlatformInfos.isMobile)
|
||||||
Matrix.of(context).client.fingerprintKey.beautified,
|
ListTile(
|
||||||
style: const TextStyle(fontFamily: 'monospace'),
|
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),
|
ListTile(
|
||||||
),
|
leading: const Icon(Icons.delete_outlined),
|
||||||
},
|
trailing: const Icon(Icons.chevron_right_outlined),
|
||||||
const Divider(height: 1),
|
title: Text(
|
||||||
ListTile(
|
L10n.of(context)!.deleteAccount,
|
||||||
leading: const Icon(Icons.tap_and_play),
|
style: const TextStyle(color: Colors.red),
|
||||||
trailing: const Icon(Icons.chevron_right_outlined),
|
),
|
||||||
title: Text(
|
onTap: controller.deleteAccountAction,
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -9,10 +9,16 @@ import 'package:matrix/matrix.dart';
|
||||||
import 'uia_request_manager.dart';
|
import 'uia_request_manager.dart';
|
||||||
|
|
||||||
extension LocalizedExceptionExtension on Object {
|
extension LocalizedExceptionExtension on Object {
|
||||||
String toLocalizedString(BuildContext context) {
|
String toLocalizedString(
|
||||||
|
BuildContext context, [
|
||||||
|
ExceptionContext? exceptionContext,
|
||||||
|
]) {
|
||||||
if (this is MatrixException) {
|
if (this is MatrixException) {
|
||||||
switch ((this as MatrixException).error) {
|
switch ((this as MatrixException).error) {
|
||||||
case MatrixError.M_FORBIDDEN:
|
case MatrixError.M_FORBIDDEN:
|
||||||
|
if (exceptionContext == ExceptionContext.changePassword) {
|
||||||
|
return L10n.of(context)!.passwordIsWrong;
|
||||||
|
}
|
||||||
return L10n.of(context)!.noPermission;
|
return L10n.of(context)!.noPermission;
|
||||||
case MatrixError.M_LIMIT_EXCEEDED:
|
case MatrixError.M_LIMIT_EXCEEDED:
|
||||||
return L10n.of(context)!.tooManyRequestsWarning;
|
return L10n.of(context)!.tooManyRequestsWarning;
|
||||||
|
@ -70,3 +76,7 @@ extension LocalizedExceptionExtension on Object {
|
||||||
return L10n.of(context)!.oopsSomethingWentWrong;
|
return L10n.of(context)!.oopsSomethingWentWrong;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ExceptionContext {
|
||||||
|
changePassword,
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue