lemmur-app-android/lib/pages/manage_account.dart

516 lines
17 KiB
Dart
Raw Normal View History

2021-01-06 00:51:19 +01:00
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
2021-01-06 18:45:56 +01:00
import 'package:image_picker/image_picker.dart';
2021-01-24 20:01:55 +01:00
import 'package:lemmy_api_client/pictrs.dart';
2021-04-05 20:14:39 +02:00
import 'package:lemmy_api_client/v3.dart';
2021-04-19 16:57:43 +02:00
import 'package:url_launcher/url_launcher.dart' as ul;
2021-01-06 00:51:19 +01:00
2021-01-06 18:45:56 +01:00
import '../hooks/delayed_loading.dart';
import '../hooks/image_picker.dart';
import '../hooks/ref.dart';
2021-01-06 00:51:19 +01:00
import '../hooks/stores.dart';
2021-03-01 14:21:45 +01:00
import '../l10n/l10n.dart';
2021-04-19 16:57:43 +02:00
import '../util/more_icon.dart';
2021-01-06 18:45:56 +01:00
import '../util/pictrs.dart';
2021-04-19 16:57:43 +02:00
import '../widgets/bottom_modal.dart';
2021-01-26 23:51:02 +01:00
import '../widgets/bottom_safe.dart';
2021-04-27 22:22:02 +02:00
import '../widgets/editor.dart';
2021-01-06 00:51:19 +01:00
2021-01-06 18:45:56 +01:00
/// Page for managing things like username, email, avatar etc
/// This page will assume the manage account is logged in and
/// its token is in AccountsStore
class ManageAccountPage extends HookWidget {
2021-01-06 00:51:19 +01:00
final String instanceHost;
final String username;
const ManageAccountPage({required this.instanceHost, required this.username});
2021-01-06 00:51:19 +01:00
@override
Widget build(BuildContext context) {
final accountStore = useAccountsStore();
2021-01-06 18:45:56 +01:00
final userFuture = useMemoized(() async {
2021-04-11 18:27:22 +02:00
final site = await LemmyApiV3(instanceHost).run(GetSite(
auth: accountStore.userDataFor(instanceHost, username)!.jwt.raw));
2021-01-06 00:51:19 +01:00
2021-08-26 00:27:50 +02:00
return site.myUser!.localUserView;
2021-01-06 00:51:19 +01:00
});
2021-04-19 16:57:43 +02:00
void _openMoreMenu() {
showBottomModal(
context: context,
builder: (context) => Column(
children: [
ListTile(
leading: const Icon(Icons.open_in_browser),
title: const Text('Open in browser'),
onTap: () async {
final userProfileUrl =
await userFuture.then((e) => e.person.actorId);
if (await ul.canLaunch(userProfileUrl)) {
await ul.launch(userProfileUrl);
2021-04-21 16:12:18 +02:00
Navigator.of(context).pop();
2021-04-19 16:57:43 +02:00
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("can't open in browser")),
);
}
},
),
],
),
);
}
2021-01-06 00:51:19 +01:00
return Scaffold(
appBar: AppBar(
title: Text('$username@$instanceHost'),
2021-04-19 16:57:43 +02:00
actions: [
IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu),
],
2021-01-06 00:51:19 +01:00
),
2021-04-05 20:14:39 +02:00
body: FutureBuilder<LocalUserSettingsView>(
2021-01-06 18:45:56 +01:00
future: userFuture,
builder: (_, userSnap) {
if (userSnap.hasError) {
return Center(child: Text('Error: ${userSnap.error?.toString()}'));
2021-01-06 00:51:19 +01:00
}
2021-01-06 18:45:56 +01:00
if (!userSnap.hasData) {
2021-01-06 00:51:19 +01:00
return const Center(child: CircularProgressIndicator());
}
return _ManageAccount(user: userSnap.data!);
2021-01-06 18:45:56 +01:00
},
),
);
}
}
class _ManageAccount extends HookWidget {
const _ManageAccount({Key? key, required this.user}) : super(key: key);
2021-01-06 18:45:56 +01:00
2021-04-05 20:14:39 +02:00
final LocalUserSettingsView user;
2021-01-06 18:45:56 +01:00
@override
Widget build(BuildContext context) {
2021-01-06 19:11:01 +01:00
final accountsStore = useAccountsStore();
2021-01-06 18:45:56 +01:00
final theme = Theme.of(context);
2021-01-06 19:11:01 +01:00
final saveDelayedLoading = useDelayedLoading();
final deleteDelayedLoading = useDelayedLoading();
2021-01-06 18:45:56 +01:00
final displayNameController =
2021-04-27 21:59:04 +02:00
useTextEditingController(text: user.person.displayName);
2021-04-05 20:14:39 +02:00
final bioController = useTextEditingController(text: user.person.bio);
final emailController =
useTextEditingController(text: user.localUser.email);
2021-01-17 17:21:46 +01:00
final matrixUserController =
2021-04-05 20:14:39 +02:00
useTextEditingController(text: user.person.matrixUserId);
final avatar = useRef(user.person.avatar);
final banner = useRef(user.person.banner);
2021-04-22 19:34:35 +02:00
final showNsfw = useState(user.localUser.showNsfw);
2021-04-27 22:22:02 +02:00
final botAccount = useState(user.person.botAccount);
final showBotAccounts = useState(user.localUser.showBotAccounts);
final showReadPosts = useState(user.localUser.showReadPosts);
2021-04-05 20:14:39 +02:00
final sendNotificationsToEmail =
useState(user.localUser.sendNotificationsToEmail);
2021-04-27 22:22:02 +02:00
// TODO: bring back changing password
// final newPasswordController = useTextEditingController();
// final newPasswordVerifyController = useTextEditingController();
// final oldPasswordController = useTextEditingController();
2021-01-06 18:45:56 +01:00
final informAcceptedAvatarRef = useRef<VoidCallback?>(null);
final informAcceptedBannerRef = useRef<VoidCallback?>(null);
2021-01-06 19:11:01 +01:00
final deleteAccountPasswordController = useTextEditingController();
2021-04-11 17:19:44 +02:00
final bioFocusNode = useFocusNode();
final emailFocusNode = useFocusNode();
final matrixUserFocusNode = useFocusNode();
final newPasswordFocusNode = useFocusNode();
2021-04-27 22:22:02 +02:00
// final verifyPasswordFocusNode = useFocusNode();
// final oldPasswordFocusNode = useFocusNode();
2021-04-11 17:19:44 +02:00
2021-04-11 18:27:22 +02:00
final token =
accountsStore.userDataFor(user.instanceHost, user.person.name)!.jwt;
2021-01-06 18:45:56 +01:00
handleSubmit() async {
2021-01-06 19:11:01 +01:00
saveDelayedLoading.start();
2021-01-06 18:45:56 +01:00
try {
2021-04-05 20:14:39 +02:00
await LemmyApiV3(user.instanceHost).run(SaveUserSettings(
2021-04-22 19:34:35 +02:00
showNsfw: showNsfw.value,
2021-04-05 20:14:39 +02:00
theme: user.localUser.theme,
2021-04-18 16:48:38 +02:00
defaultSortType: user.localUser.defaultSortType,
defaultListingType: user.localUser.defaultListingType,
2021-04-05 20:14:39 +02:00
lang: user.localUser.lang,
2021-04-18 16:48:38 +02:00
showAvatars: user.localUser.showAvatars,
2021-04-27 22:22:02 +02:00
botAccount: botAccount.value,
showBotAccounts: showBotAccounts.value,
showReadPosts: showReadPosts.value,
2021-01-24 20:01:55 +01:00
sendNotificationsToEmail: sendNotificationsToEmail.value,
auth: token.raw,
avatar: avatar.current,
banner: banner.current,
matrixUserId: matrixUserController.text.isEmpty
? null
: matrixUserController.text,
2021-04-27 21:59:04 +02:00
displayName: displayNameController.text.isEmpty
2021-01-24 20:01:55 +01:00
? null
: displayNameController.text,
bio: bioController.text.isEmpty ? null : bioController.text,
email: emailController.text.isEmpty ? null : emailController.text,
));
informAcceptedAvatarRef.current?.call();
informAcceptedBannerRef.current?.call();
2021-03-10 08:34:30 +01:00
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
2021-01-06 19:11:01 +01:00
content: Text('User settings saved'),
));
2021-01-06 18:45:56 +01:00
} on Exception catch (err) {
2021-03-10 08:34:30 +01:00
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
2021-01-06 18:45:56 +01:00
content: Text(err.toString()),
));
2021-01-17 17:21:46 +01:00
} finally {
saveDelayedLoading.cancel();
2021-01-06 18:45:56 +01:00
}
2021-01-06 19:11:01 +01:00
}
deleteAccountDialog() async {
final confirmDelete = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
2021-03-03 13:16:05 +01:00
title: Text(
'${L10n.of(context)!.delete_account} @${user.instanceHost}@${user.person.name}'),
2021-01-06 19:11:01 +01:00
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(L10n.of(context)!.delete_account_confirm),
2021-03-03 13:16:05 +01:00
const SizedBox(height: 10),
2021-01-06 19:11:01 +01:00
TextField(
controller: deleteAccountPasswordController,
2021-04-11 17:19:44 +02:00
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.visiblePassword,
2021-01-06 19:11:01 +01:00
obscureText: true,
2021-03-03 13:16:05 +01:00
decoration:
InputDecoration(hintText: L10n.of(context)!.password),
2021-01-06 19:11:01 +01:00
)
],
),
actions: [
2021-02-09 15:12:13 +01:00
TextButton(
2021-01-06 19:11:01 +01:00
onPressed: () => Navigator.of(context).pop(false),
child: Text(L10n.of(context)!.no),
2021-01-06 19:11:01 +01:00
),
2021-02-09 15:12:13 +01:00
TextButton(
2021-01-06 19:11:01 +01:00
onPressed: () => Navigator.of(context).pop(true),
child: Text(L10n.of(context)!.yes),
2021-01-06 19:11:01 +01:00
),
],
),
) ??
false;
if (confirmDelete) {
deleteDelayedLoading.start();
try {
2021-04-05 20:14:39 +02:00
await LemmyApiV3(user.instanceHost).run(DeleteAccount(
2021-01-24 20:01:55 +01:00
password: deleteAccountPasswordController.text,
auth: token.raw,
));
2021-01-06 19:11:01 +01:00
2021-04-05 20:14:39 +02:00
await accountsStore.removeAccount(
user.instanceHost, user.person.name);
2021-01-06 19:11:01 +01:00
Navigator.of(context).pop();
} on Exception catch (err) {
2021-03-10 08:34:30 +01:00
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
2021-01-06 19:11:01 +01:00
content: Text(err.toString()),
));
}
deleteDelayedLoading.cancel();
} else {
deleteAccountPasswordController.clear();
}
2021-01-06 18:45:56 +01:00
}
return ListView(
2021-01-17 18:57:57 +01:00
padding: const EdgeInsets.symmetric(horizontal: 15),
2021-01-06 18:45:56 +01:00
children: [
_ImagePicker(
user: user,
name: L10n.of(context)!.avatar,
2021-01-06 18:45:56 +01:00
initialUrl: avatar.current,
onChange: (value) => avatar.current = value,
2021-01-08 09:47:59 +01:00
informAcceptedRef: informAcceptedAvatarRef,
2021-01-06 18:45:56 +01:00
),
const SizedBox(height: 8),
_ImagePicker(
user: user,
name: L10n.of(context)!.banner,
2021-01-06 18:45:56 +01:00
initialUrl: banner.current,
onChange: (value) => banner.current = value,
2021-01-08 09:47:59 +01:00
informAcceptedRef: informAcceptedBannerRef,
2021-01-06 18:45:56 +01:00
),
const SizedBox(height: 8),
Text(L10n.of(context)!.display_name, style: theme.textTheme.headline6),
2021-04-11 17:19:44 +02:00
TextField(
controller: displayNameController,
onSubmitted: (_) => bioFocusNode.requestFocus(),
),
2021-01-06 18:45:56 +01:00
const SizedBox(height: 8),
Text(L10n.of(context)!.bio, style: theme.textTheme.headline6),
2021-04-27 22:22:02 +02:00
Editor(
2021-01-06 18:45:56 +01:00
controller: bioController,
2021-04-11 17:19:44 +02:00
focusNode: bioFocusNode,
onSubmitted: (_) => emailFocusNode.requestFocus(),
2021-04-27 22:22:02 +02:00
instanceHost: user.instanceHost,
2021-01-06 18:45:56 +01:00
maxLines: 10,
),
const SizedBox(height: 8),
Text(L10n.of(context)!.email, style: theme.textTheme.headline6),
2021-04-11 17:19:44 +02:00
TextField(
focusNode: emailFocusNode,
controller: emailController,
autofillHints: const [AutofillHints.email],
keyboardType: TextInputType.emailAddress,
onSubmitted: (_) => matrixUserFocusNode.requestFocus(),
),
2021-01-06 18:45:56 +01:00
const SizedBox(height: 8),
Text(L10n.of(context)!.matrix_user, style: theme.textTheme.headline6),
2021-04-11 17:19:44 +02:00
TextField(
focusNode: matrixUserFocusNode,
controller: matrixUserController,
onSubmitted: (_) => newPasswordFocusNode.requestFocus(),
),
2021-01-17 17:21:46 +01:00
const SizedBox(height: 8),
2021-04-27 22:22:02 +02:00
// Text(L10n.of(context)!.new_password, style: theme.textTheme.headline6),
// TextField(
// focusNode: newPasswordFocusNode,
// controller: newPasswordController,
// autofillHints: const [AutofillHints.newPassword],
// keyboardType: TextInputType.visiblePassword,
// obscureText: true,
// onSubmitted: (_) => verifyPasswordFocusNode.requestFocus(),
// ),
// const SizedBox(height: 8),
// Text(L10n.of(context)!.verify_password,
// style: theme.textTheme.headline6),
// TextField(
// focusNode: verifyPasswordFocusNode,
// controller: newPasswordVerifyController,
// autofillHints: const [AutofillHints.newPassword],
// keyboardType: TextInputType.visiblePassword,
// obscureText: true,
// onSubmitted: (_) => oldPasswordFocusNode.requestFocus(),
// ),
// const SizedBox(height: 8),
// Text(L10n.of(context)!.old_password, style: theme.textTheme.headline6),
// TextField(
// focusNode: oldPasswordFocusNode,
// controller: oldPasswordController,
// autofillHints: const [AutofillHints.password],
// keyboardType: TextInputType.visiblePassword,
// obscureText: true,
// ),
// const SizedBox(height: 8),
SwitchListTile.adaptive(
value: showNsfw.value,
onChanged: (checked) {
showNsfw.value = checked;
},
title: Text(L10n.of(context)!.show_nsfw),
dense: true,
2021-01-17 17:21:46 +01:00
),
const SizedBox(height: 8),
2021-04-27 22:22:02 +02:00
SwitchListTile.adaptive(
value: botAccount.value,
onChanged: (checked) {
botAccount.value = checked;
},
2021-04-27 22:42:52 +02:00
title: Text(L10n.of(context)!.bot_account),
2021-04-27 22:22:02 +02:00
dense: true,
2021-01-17 17:21:46 +01:00
),
const SizedBox(height: 8),
2021-04-27 22:22:02 +02:00
SwitchListTile.adaptive(
value: showBotAccounts.value,
onChanged: (checked) {
showBotAccounts.value = checked;
},
2021-04-27 22:42:52 +02:00
title: Text(L10n.of(context)!.show_bot_accounts),
2021-04-27 22:22:02 +02:00
dense: true,
2021-01-17 17:21:46 +01:00
),
const SizedBox(height: 8),
2021-04-22 19:34:35 +02:00
SwitchListTile.adaptive(
2021-04-27 22:22:02 +02:00
value: showReadPosts.value,
2021-04-22 19:34:35 +02:00
onChanged: (checked) {
2021-04-27 22:22:02 +02:00
showReadPosts.value = checked;
2021-04-22 19:34:35 +02:00
},
2021-04-27 22:42:52 +02:00
title: Text(L10n.of(context)!.show_read_posts),
2021-04-22 19:34:35 +02:00
dense: true,
),
const SizedBox(height: 8),
2021-04-18 16:32:35 +02:00
SwitchListTile.adaptive(
2021-01-17 17:21:46 +01:00
value: sendNotificationsToEmail.value,
onChanged: (checked) {
2021-04-18 16:32:35 +02:00
sendNotificationsToEmail.value = checked;
},
title: Text(L10n.of(context)!.send_notifications_to_email),
2021-01-17 17:21:46 +01:00
dense: true,
),
const SizedBox(height: 8),
2021-01-06 18:45:56 +01:00
ElevatedButton(
2021-01-06 19:11:01 +01:00
onPressed: saveDelayedLoading.loading ? null : handleSubmit,
child: saveDelayedLoading.loading
2021-01-06 18:45:56 +01:00
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(),
)
: Text(L10n.of(context)!.save),
2021-01-06 18:45:56 +01:00
),
const SizedBox(height: 8),
ElevatedButton(
2021-01-06 19:11:01 +01:00
onPressed: deleteAccountDialog,
2021-01-06 18:45:56 +01:00
style: ElevatedButton.styleFrom(
primary: Colors.red,
),
child: Text(L10n.of(context)!.delete_account.toUpperCase()),
2021-01-06 18:45:56 +01:00
),
2021-01-26 23:16:47 +01:00
const BottomSafe(),
2021-01-06 18:45:56 +01:00
],
);
}
}
/// Picker and cleanuper for local images uploaded to pictrs
class _ImagePicker extends HookWidget {
final String name;
final String? initialUrl;
2021-04-05 20:14:39 +02:00
final LocalUserSettingsView user;
final ValueChanged<String?>? onChange;
2021-01-06 18:45:56 +01:00
/// _ImagePicker will set the ref to a callback that can inform _ImagePicker
/// that the current picture is accepted
2021-01-08 09:47:59 +01:00
/// and should no longer allow for deletion of it
final Ref<VoidCallback?> informAcceptedRef;
2021-01-06 18:45:56 +01:00
const _ImagePicker({
Key? key,
required this.initialUrl,
required this.name,
required this.user,
required this.onChange,
required this.informAcceptedRef,
}) : super(key: key);
2021-01-06 18:45:56 +01:00
@override
Widget build(BuildContext context) {
// this is in case the passed initialUrl is changed,
// basically saves the very first initialUrl
final initialUrl = useRef(this.initialUrl);
2021-01-06 18:45:56 +01:00
final theme = Theme.of(context);
final url = useState(initialUrl.current);
final pictrsDeleteToken = useState<PictrsUploadFile?>(null);
2021-01-06 18:45:56 +01:00
final imagePicker = useImagePicker();
final accountsStore = useAccountsStore();
final delayedLoading = useDelayedLoading();
2021-01-06 18:45:56 +01:00
uploadImage() async {
try {
final pic = await imagePicker.getImage(source: ImageSource.gallery);
// pic is null when the picker was cancelled
if (pic != null) {
delayedLoading.start();
2021-01-24 20:01:55 +01:00
final upload = await PictrsApi(user.instanceHost).upload(
filePath: pic.path,
auth: accountsStore
2021-04-11 18:27:22 +02:00
.userDataFor(user.instanceHost, user.person.name)!
.jwt
.raw,
2021-01-24 20:01:55 +01:00
);
2021-01-06 18:45:56 +01:00
pictrsDeleteToken.value = upload.files[0];
url.value =
pathToPictrs(user.instanceHost, pictrsDeleteToken.value!.file);
2021-01-06 18:45:56 +01:00
2021-04-11 00:20:47 +02:00
onChange?.call(url.value);
2021-01-06 18:45:56 +01:00
}
} on Exception catch (_) {
2021-03-10 08:34:30 +01:00
ScaffoldMessenger.of(context).showSnackBar(
2021-01-06 18:45:56 +01:00
const SnackBar(content: Text('Failed to upload image')));
}
delayedLoading.cancel();
2021-01-06 18:45:56 +01:00
}
removePicture({
bool updateState = true,
required PictrsUploadFile pictrsToken,
}) {
PictrsApi(user.instanceHost).delete(pictrsToken).catchError((_) {});
2021-01-06 18:45:56 +01:00
if (updateState) {
pictrsDeleteToken.value = null;
url.value = initialUrl.current;
onChange?.call(url.value);
2021-01-06 18:45:56 +01:00
}
}
useEffect(() {
informAcceptedRef.current = () {
pictrsDeleteToken.value = null;
initialUrl.current = url.value;
};
return () {
2021-01-06 18:45:56 +01:00
// remove picture from pictrs when exiting
if (pictrsDeleteToken.value != null) {
removePicture(
updateState: false,
pictrsToken: pictrsDeleteToken.value!,
);
}
};
}, []);
2021-01-06 18:45:56 +01:00
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(name, style: theme.textTheme.headline6),
if (pictrsDeleteToken.value == null)
2021-01-06 00:51:19 +01:00
ElevatedButton(
onPressed: delayedLoading.loading ? null : uploadImage,
child: delayedLoading.loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator())
: Row(
children: const [Text('upload'), Icon(Icons.publish)],
),
2021-01-06 18:45:56 +01:00
)
else
IconButton(
icon: const Icon(Icons.close),
onPressed: () =>
removePicture(pictrsToken: pictrsDeleteToken.value!),
2021-01-06 18:45:56 +01:00
)
],
),
if (url.value != null)
CachedNetworkImage(
imageUrl: url.value!,
2021-01-06 18:45:56 +01:00
errorWidget: (_, __, ___) => const Icon(Icons.error),
),
],
2021-01-06 00:51:19 +01:00
);
}
}