design: Improve user permission settings

pull/1031/head
krille-chan 2 years ago
parent ac7e424b7b
commit 216d3bd403
No known key found for this signature in database

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

@ -2,7 +2,6 @@ import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.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/pages/chat_permissions_settings/chat_permissions_settings_view.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/widgets/permission_slider_dialog.dart';
class ChatPermissionsSettings extends StatefulWidget { class ChatPermissionsSettings extends StatefulWidget {
const ChatPermissionsSettings({super.key}); const ChatPermissionsSettings({super.key});
@ -35,30 +35,9 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
); );
return; return;
} }
newLevel ??= int.tryParse( newLevel ??= await showPermissionChooser(
(await showTextInputDialog( context,
context: context, currentLevel: currentLevel,
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 ??
'',
); );
if (newLevel == null) return; if (newLevel == null) return;
final content = Map<String, dynamic>.from( final content = Map<String, dynamic>.from(

@ -17,7 +17,6 @@ enum UserBottomSheetAction {
ban, ban,
kick, kick,
unban, unban,
permission,
message, message,
ignore, ignore,
} }
@ -201,30 +200,6 @@ class UserBottomSheetController extends State<UserBottomSheet> {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
break; 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: case UserBottomSheetAction.message:
Navigator.of(context).pop(); Navigator.of(context).pop();
// Workaround for https://github.com/flutter/flutter/issues/27495 // Workaround for https://github.com/flutter/flutter/issues/27495
@ -270,6 +245,35 @@ class UserBottomSheetController extends State<UserBottomSheet> {
Navigator.of(context).pop(); 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 @override
Widget build(BuildContext context) => UserBottomSheetView(this); Widget build(BuildContext context) => UserBottomSheetView(this);
} }

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

@ -4,77 +4,31 @@ import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.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( Future<int?> showPermissionChooser(
BuildContext context, { BuildContext context, {
int currentLevel = 0, int currentLevel = 0,
}) async { }) async {
final permissionLevel = await showConfirmationDialog( final customLevel = await showTextInputDialog(
context: context, context: context,
title: L10n.of(context)!.setPermissionsLevel, title: L10n.of(context)!.setPermissionsLevel,
actions: PermissionLevel.values textFields: [
.map( DialogTextField(
(level) => AlertDialogAction( initialText: currentLevel.toString(),
key: level, keyboardType: TextInputType.number,
label: level.toLocalizedString(context), autocorrect: false,
), validator: (text) {
) if (text == null) {
.toList(), 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; if (customLevel == null) return null;
return int.tryParse(customLevel.first);
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);
}
} }

Loading…
Cancel
Save