From 216d3bd40313879fcf844672d1ca1da2cb0b0557 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Mon, 15 Apr 2024 09:50:50 +0200 Subject: [PATCH] design: Improve user permission settings --- assets/l10n/intl_en.arb | 1 + .../chat_permissions_settings.dart | 29 +- .../user_bottom_sheet/user_bottom_sheet.dart | 54 +- .../user_bottom_sheet_view.dart | 467 ++++++++++-------- lib/widgets/permission_slider_dialog.dart | 86 +--- 5 files changed, 312 insertions(+), 325 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 56cf36a0..f93b7004 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -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", diff --git a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart index 75ca11d4..6ea94c10 100644 --- a/lib/pages/chat_permissions_settings/chat_permissions_settings.dart +++ b/lib/pages/chat_permissions_settings/chat_permissions_settings.dart @@ -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 { ); 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.from( diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart index ae6c1c92..98ad0e1c 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet.dart @@ -17,7 +17,6 @@ enum UserBottomSheetAction { ban, kick, unban, - permission, message, ignore, } @@ -201,30 +200,6 @@ class UserBottomSheetController extends State { 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 { 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); } diff --git a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart index 13319d0e..5735b814 100644 --- a/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart +++ b/lib/pages/user_bottom_sheet/user_bottom_sheet_view.dart @@ -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( + 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( + 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), - ), - ), - ], + ); + }, ), ), ); diff --git a/lib/widgets/permission_slider_dialog.dart b/lib/widgets/permission_slider_dialog.dart index cc6e9303..bc6fd9c2 100644 --- a/lib/widgets/permission_slider_dialog.dart +++ b/lib/widgets/permission_slider_dialog.dart @@ -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 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); }