design: Improve user permission settings

This commit is contained in:
krille-chan 2024-04-15 09:50:50 +02:00
parent ac7e424b7b
commit 216d3bd403
No known key found for this signature in database
5 changed files with 312 additions and 325 deletions

View file

@ -2525,6 +2525,7 @@
"@thisDevice": {},
"initAppError": "An error occured while init the app",
"@initAppError": {},
"userRole": "User role",
"minimumPowerLevel": "{level} is the minimum power level.",
"@minimumPowerLevel": {
"type": "text",

View file

@ -2,7 +2,6 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.dart';
@ -10,6 +9,7 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/pages/chat_permissions_settings/chat_permissions_settings_view.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/permission_slider_dialog.dart';
class ChatPermissionsSettings extends StatefulWidget {
const ChatPermissionsSettings({super.key});
@ -35,30 +35,9 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
);
return;
}
newLevel ??= int.tryParse(
(await showTextInputDialog(
context: context,
title: L10n.of(context)!.setPermissionsLevel,
textFields: [
DialogTextField(
initialText: currentLevel.toString(),
keyboardType: TextInputType.number,
autocorrect: false,
validator: (text) {
if (text == null) {
return L10n.of(context)!.pleaseEnterANumber;
}
final level = int.tryParse(text);
if (level == null || level < 0) {
return L10n.of(context)!.pleaseEnterANumber;
}
return null;
},
),
],
))
?.singleOrNull ??
'',
newLevel ??= await showPermissionChooser(
context,
currentLevel: currentLevel,
);
if (newLevel == null) return;
final content = Map<String, dynamic>.from(

View file

@ -17,7 +17,6 @@ enum UserBottomSheetAction {
ban,
kick,
unban,
permission,
message,
ignore,
}
@ -201,30 +200,6 @@ class UserBottomSheetController extends State<UserBottomSheet> {
Navigator.of(context).pop();
}
break;
case UserBottomSheetAction.permission:
if (user == null) throw ('User must not be null for this action!');
final newPermission = await showPermissionChooser(
context,
currentLevel: user.powerLevel,
);
if (newPermission != null) {
if (newPermission == 100 &&
await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.no,
message: L10n.of(context)!.makeAdminDescription,
) !=
OkCancelResult.ok) break;
await showFutureLoadingDialog(
context: context,
future: () => user.setPower(newPermission),
);
Navigator.of(context).pop();
}
break;
case UserBottomSheetAction.message:
Navigator.of(context).pop();
// Workaround for https://github.com/flutter/flutter/issues/27495
@ -270,6 +245,35 @@ class UserBottomSheetController extends State<UserBottomSheet> {
Navigator.of(context).pop();
}
void setPowerLevel(int? newLevel) async {
final user = widget.user;
if (user == null) throw ('User must not be null for this action!');
final level = newLevel ??
await showPermissionChooser(
context,
currentLevel: user.powerLevel,
);
if (level == null) return;
if (level == 100) {
final consent = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context)!.areYouSure,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.no,
message: L10n.of(context)!.makeAdminDescription,
);
if (consent != OkCancelResult.ok) return;
}
await showFutureLoadingDialog(
context: context,
future: () => user.setPower(level),
);
}
@override
Widget build(BuildContext context) => UserBottomSheetView(this);
}

View file

@ -104,228 +104,277 @@ class UserBottomSheetView extends StatelessWidget {
),
],
),
body: ListView(
children: [
if (user?.membership == Membership.knock)
Padding(
padding: const EdgeInsets.all(12.0),
child: Material(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: ListTile(
minVerticalPadding: 16,
title: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
L10n.of(context)!
.userWouldLikeToChangeTheChat(displayname),
body: StreamBuilder<Object>(
stream: user?.room.client.onSync.stream.where(
(syncUpdate) =>
syncUpdate.rooms?.join?[user.room.id]?.timeline?.events?.any(
(state) => state.type == EventTypes.RoomPowerLevels,
) ??
false,
),
builder: (context, snapshot) {
return ListView(
children: [
if (user?.membership == Membership.knock)
Padding(
padding: const EdgeInsets.all(12.0),
child: Material(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
child: ListTile(
minVerticalPadding: 16,
title: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
L10n.of(context)!
.userWouldLikeToChangeTheChat(displayname),
),
),
subtitle: Row(
children: [
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.background,
foregroundColor:
Theme.of(context).colorScheme.primary,
),
onPressed: controller.knockAccept,
icon: const Icon(Icons.check_outlined),
label: Text(L10n.of(context)!.accept),
),
const SizedBox(width: 12),
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.errorContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onErrorContainer,
),
onPressed: controller.knockDecline,
icon: const Icon(Icons.cancel_outlined),
label: Text(L10n.of(context)!.decline),
),
],
),
),
),
subtitle: Row(
children: [
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.background,
foregroundColor:
Theme.of(context).colorScheme.primary,
),
Row(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Material(
elevation: Theme.of(context)
.appBarTheme
.scrolledUnderElevation ??
4,
shadowColor: Theme.of(context).appBarTheme.shadowColor,
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(
Avatar.defaultSize * 2.5,
),
onPressed: controller.knockAccept,
icon: const Icon(Icons.check_outlined),
label: Text(L10n.of(context)!.accept),
),
const SizedBox(width: 12),
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.errorContainer,
foregroundColor:
Theme.of(context).colorScheme.onErrorContainer,
child: Avatar(
mxContent: avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
fontSize: 18 * 2.5,
),
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: () => FluffyShare.share(
'https://matrix.to/#/$userId',
context,
),
icon: Icon(
Icons.adaptive.share_outlined,
size: 16,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onBackground,
),
label: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 18),
),
),
onPressed: controller.knockDecline,
icon: const Icon(Icons.cancel_outlined),
label: Text(L10n.of(context)!.decline),
TextButton.icon(
onPressed: () => FluffyShare.share(
userId,
context,
copyOnly: true,
),
icon: const Icon(
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.secondary,
),
label: Text(
userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 12),
),
),
],
),
),
],
),
if (userId != client.userID)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: ElevatedButton.icon(
onPressed: () => controller
.participantAction(UserBottomSheetAction.message),
icon: const Icon(Icons.forum_outlined),
label: Text(
controller.widget.user == null
? L10n.of(context)!.startConversation
: L10n.of(context)!.sendAMessage,
),
),
),
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
final status = presence?.statusMsg;
if (status == null || status.isEmpty) {
return const SizedBox.shrink();
}
return ListTile(
title: SelectableLinkify(
text: status,
style: const TextStyle(fontSize: 16),
options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle(
color: Colors.blueAccent,
decorationColor: Colors.blueAccent,
),
onOpen: (url) =>
UrlLauncher(context, url.url).launchUrl(),
),
);
},
),
if (controller.widget.onMention != null)
ListTile(
leading: const Icon(Icons.alternate_email_outlined),
title: Text(L10n.of(context)!.mention),
onTap: () => controller
.participantAction(UserBottomSheetAction.mention),
),
if (user != null) ...[
Divider(height: 1, color: Theme.of(context).dividerColor),
ListTile(
title: Text(
'${L10n.of(context)!.userRole} (${user.powerLevel})',
),
leading: const Icon(Icons.person_outlined),
trailing: DropdownButton<int>(
onChanged: user.canChangePowerLevel
? controller.setPowerLevel
: null,
value: {0, 50, 100}.contains(user.powerLevel)
? user.powerLevel
: null,
items: [
DropdownMenuItem(
value: 0,
child: Text(L10n.of(context)!.user),
),
DropdownMenuItem(
value: 50,
child: Text(L10n.of(context)!.moderator),
),
DropdownMenuItem(
value: 100,
child: Text(L10n.of(context)!.admin),
),
DropdownMenuItem(
value: null,
child: Text(L10n.of(context)!.custom),
),
],
),
),
),
),
Row(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Material(
elevation:
Theme.of(context).appBarTheme.scrolledUnderElevation ??
4,
shadowColor: Theme.of(context).appBarTheme.shadowColor,
shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(
Avatar.defaultSize * 2.5,
),
Divider(height: 1, color: Theme.of(context).dividerColor),
],
if (user != null && user.canKick)
ListTile(
textColor: Theme.of(context).colorScheme.error,
iconColor: Theme.of(context).colorScheme.error,
title: Text(L10n.of(context)!.kickFromChat),
leading: const Icon(Icons.exit_to_app_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.kick),
),
if (user != null &&
user.canBan &&
user.membership != Membership.ban)
ListTile(
textColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
title: Text(L10n.of(context)!.banFromChat),
leading: const Icon(Icons.warning_sharp),
onTap: () =>
controller.participantAction(UserBottomSheetAction.ban),
)
else if (user != null &&
user.canBan &&
user.membership == Membership.ban)
ListTile(
title: Text(L10n.of(context)!.unbanFromChat),
leading: const Icon(Icons.warning_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.unban),
),
if (user != null && user.id != client.userID)
ListTile(
textColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
title: Text(L10n.of(context)!.reportUser),
leading: const Icon(Icons.report_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.report),
),
if (profileSearchError != null)
ListTile(
leading: const Icon(
Icons.warning_outlined,
color: Colors.orange,
),
child: Avatar(
mxContent: avatarUrl,
name: displayname,
size: Avatar.defaultSize * 2.5,
fontSize: 18 * 2.5,
subtitle: Text(
L10n.of(context)!.profileNotFound,
style: const TextStyle(color: Colors.orange),
),
),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
onPressed: () => FluffyShare.share(
'https://matrix.to/#/$userId',
context,
),
icon: Icon(
Icons.adaptive.share_outlined,
size: 16,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onBackground,
),
label: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 18),
),
),
TextButton.icon(
onPressed: () => FluffyShare.share(
userId,
context,
copyOnly: true,
),
icon: const Icon(
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.secondary,
),
label: Text(
userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 12),
),
),
],
),
),
],
),
if (userId != client.userID)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: ElevatedButton.icon(
onPressed: () => controller
.participantAction(UserBottomSheetAction.message),
icon: const Icon(Icons.forum_outlined),
label: Text(
controller.widget.user == null
? L10n.of(context)!.startConversation
: L10n.of(context)!.sendAMessage,
),
),
),
PresenceBuilder(
userId: userId,
client: client,
builder: (context, presence) {
final status = presence?.statusMsg;
if (status == null || status.isEmpty) {
return const SizedBox.shrink();
}
return ListTile(
title: SelectableLinkify(
text: status,
style: const TextStyle(fontSize: 16),
options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle(
color: Colors.blueAccent,
decorationColor: Colors.blueAccent,
),
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
),
);
},
),
if (controller.widget.onMention != null)
ListTile(
leading: const Icon(Icons.alternate_email_outlined),
title: Text(L10n.of(context)!.mention),
onTap: () =>
controller.participantAction(UserBottomSheetAction.mention),
),
if (user != null && user.canChangePowerLevel)
ListTile(
title: Text(L10n.of(context)!.setPermissionsLevel),
leading: const Icon(Icons.edit_attributes_outlined),
onTap: () => controller
.participantAction(UserBottomSheetAction.permission),
),
if (user != null && user.canKick)
ListTile(
title: Text(L10n.of(context)!.kickFromChat),
leading: const Icon(Icons.exit_to_app_outlined),
onTap: () =>
controller.participantAction(UserBottomSheetAction.kick),
),
if (user != null &&
user.canBan &&
user.membership != Membership.ban)
ListTile(
title: Text(L10n.of(context)!.banFromChat),
leading: const Icon(Icons.warning_sharp),
onTap: () =>
controller.participantAction(UserBottomSheetAction.ban),
)
else if (user != null &&
user.canBan &&
user.membership == Membership.ban)
ListTile(
title: Text(L10n.of(context)!.unbanFromChat),
leading: const Icon(Icons.warning_outlined),
onTap: () =>
controller.participantAction(UserBottomSheetAction.unban),
),
if (user != null && user.id != client.userID)
ListTile(
textColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
title: Text(L10n.of(context)!.reportUser),
leading: const Icon(Icons.report_outlined),
onTap: () =>
controller.participantAction(UserBottomSheetAction.report),
),
if (profileSearchError != null)
ListTile(
leading: const Icon(
Icons.warning_outlined,
color: Colors.orange,
),
subtitle: Text(
L10n.of(context)!.profileNotFound,
style: const TextStyle(color: Colors.orange),
),
),
],
);
},
),
),
);

View file

@ -4,77 +4,31 @@ import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
enum PermissionLevel {
user,
moderator,
admin,
custom,
}
extension on PermissionLevel {
String toLocalizedString(BuildContext context) {
switch (this) {
case PermissionLevel.user:
return L10n.of(context)!.user;
case PermissionLevel.moderator:
return L10n.of(context)!.moderator;
case PermissionLevel.admin:
return L10n.of(context)!.admin;
case PermissionLevel.custom:
default:
return L10n.of(context)!.custom;
}
}
}
Future<int?> showPermissionChooser(
BuildContext context, {
int currentLevel = 0,
}) async {
final permissionLevel = await showConfirmationDialog(
final customLevel = await showTextInputDialog(
context: context,
title: L10n.of(context)!.setPermissionsLevel,
actions: PermissionLevel.values
.map(
(level) => AlertDialogAction(
key: level,
label: level.toLocalizedString(context),
),
)
.toList(),
textFields: [
DialogTextField(
initialText: currentLevel.toString(),
keyboardType: TextInputType.number,
autocorrect: false,
validator: (text) {
if (text == null) {
return L10n.of(context)!.pleaseEnterANumber;
}
final level = int.tryParse(text);
if (level == null || level < 0) {
return L10n.of(context)!.pleaseEnterANumber;
}
return null;
},
),
],
);
if (permissionLevel == null) return null;
switch (permissionLevel) {
case PermissionLevel.user:
return 0;
case PermissionLevel.moderator:
return 50;
case PermissionLevel.admin:
return 100;
case PermissionLevel.custom:
final customLevel = await showTextInputDialog(
context: context,
title: L10n.of(context)!.setPermissionsLevel,
textFields: [
DialogTextField(
initialText: currentLevel.toString(),
keyboardType: TextInputType.number,
autocorrect: false,
validator: (text) {
if (text == null) {
return L10n.of(context)!.pleaseEnterANumber;
}
final level = int.tryParse(text);
if (level == null || level < 0) {
return L10n.of(context)!.pleaseEnterANumber;
}
return null;
},
),
],
);
if (customLevel == null) return null;
return int.tryParse(customLevel.first);
}
if (customLevel == null) return null;
return int.tryParse(customLevel.first);
}