diff --git a/lib/components/dialogs/adaptive_flat_button.dart b/lib/components/dialogs/adaptive_flat_button.dart new file mode 100644 index 00000000..808ffb4c --- /dev/null +++ b/lib/components/dialogs/adaptive_flat_button.dart @@ -0,0 +1,32 @@ +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class AdaptiveFlatButton extends StatelessWidget { + final Widget child; + final Color textColor; + final Function onPressed; + + const AdaptiveFlatButton({ + Key key, + this.child, + this.textColor, + this.onPressed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (PlatformInfos.isCupertinoStyle) { + return CupertinoDialogAction( + child: child, + onPressed: onPressed, + textStyle: textColor != null ? TextStyle(color: textColor) : null, + ); + } + return FlatButton( + child: child, + textColor: textColor, + onPressed: onPressed, + ); + } +} \ No newline at end of file diff --git a/lib/components/dialogs/key_verification_dialog.dart b/lib/components/dialogs/key_verification_dialog.dart index 96c94ede..03a62588 100644 --- a/lib/components/dialogs/key_verification_dialog.dart +++ b/lib/components/dialogs/key_verification_dialog.dart @@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import '../avatar.dart'; +import 'adaptive_flat_button.dart'; import 'simple_dialogs.dart'; import '../../utils/string_color.dart'; @@ -121,14 +122,14 @@ class _KeyVerificationPageState extends State { mainAxisSize: MainAxisSize.min, ), ); - buttons.add(_AdaptiveFlatButton( + buttons.add(AdaptiveFlatButton( child: Text(L10n.of(context).submit), onPressed: () { input = textEditingController.text; checkInput(); }, )); - buttons.add(_AdaptiveFlatButton( + buttons.add(AdaptiveFlatButton( child: Text(L10n.of(context).skip), onPressed: () => widget.request.openSSSS(skip: true), )); @@ -140,11 +141,11 @@ class _KeyVerificationPageState extends State { style: TextStyle(fontSize: 20)), margin: EdgeInsets.only(left: 8.0, right: 8.0), ); - buttons.add(_AdaptiveFlatButton( + buttons.add(AdaptiveFlatButton( child: Text(L10n.of(context).accept), onPressed: () => widget.request.acceptVerification(), )); - buttons.add(_AdaptiveFlatButton( + buttons.add(AdaptiveFlatButton( child: Text(L10n.of(context).reject), onPressed: () { widget.request.rejectVerification().then((_) { @@ -204,11 +205,11 @@ class _KeyVerificationPageState extends State { ], mainAxisSize: MainAxisSize.min, ); - buttons.add(_AdaptiveFlatButton( + buttons.add(AdaptiveFlatButton( child: Text(L10n.of(context).theyMatch), onPressed: () => widget.request.acceptSas(), )); - buttons.add(_AdaptiveFlatButton( + buttons.add(AdaptiveFlatButton( textColor: Colors.red, child: Text(L10n.of(context).theyDontMatch), onPressed: () => widget.request.rejectSas(), @@ -244,7 +245,7 @@ class _KeyVerificationPageState extends State { ], mainAxisSize: MainAxisSize.min, ); - buttons.add(_AdaptiveFlatButton( + buttons.add(AdaptiveFlatButton( child: Text(L10n.of(context).close), onPressed: () => Navigator.of(context).pop(), )); @@ -359,32 +360,3 @@ class _Emoji extends StatelessWidget { ); } } - -class _AdaptiveFlatButton extends StatelessWidget { - final Widget child; - final Color textColor; - final Function onPressed; - - const _AdaptiveFlatButton({ - Key key, - this.child, - this.textColor, - this.onPressed, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (PlatformInfos.isCupertinoStyle) { - return CupertinoDialogAction( - child: child, - onPressed: onPressed, - textStyle: textColor != null ? TextStyle(color: textColor) : null, - ); - } - return FlatButton( - child: child, - textColor: textColor, - onPressed: onPressed, - ); - } -} diff --git a/lib/components/dialogs/permission_slider_dialog.dart b/lib/components/dialogs/permission_slider_dialog.dart new file mode 100644 index 00000000..137e9cba --- /dev/null +++ b/lib/components/dialogs/permission_slider_dialog.dart @@ -0,0 +1,85 @@ +import 'package:fluffychat/components/dialogs/adaptive_flat_button.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class PermissionSliderDialog extends StatefulWidget { + const PermissionSliderDialog({Key key, this.initialPermission = 0}) + : super(key: key); + + Future show(BuildContext context) => PlatformInfos.isCupertinoStyle + ? showCupertinoDialog(context: context, builder: (context) => this) + : showDialog(context: context, builder: (context) => this); + + final int initialPermission; + @override + _PermissionSliderDialogState createState() => _PermissionSliderDialogState(); +} + +class _PermissionSliderDialogState extends State { + int _permission; + @override + void initState() { + _permission = widget.initialPermission; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final slider = PlatformInfos.isCupertinoStyle + ? CupertinoSlider( + value: _permission.toDouble(), + onChanged: (d) => setState(() => _permission = d.round()), + max: 100.0, + min: 0.0, + ) + : Slider( + value: _permission.toDouble(), + onChanged: (d) => setState(() => _permission = d.round()), + max: 100.0, + min: 0.0, + ); + final title = Text( + L10n.of(context).setPermissionsLevel, + textAlign: TextAlign.center, + ); + final content = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Level: ' + + (_permission == 100 + ? '$_permission (${L10n.of(context).admin})' + : _permission >= 50 + ? '$_permission (${L10n.of(context).moderator})' + : _permission.toString())), + Container( + height: 56, + child: slider, + ), + ], + ); + final buttons = [ + AdaptiveFlatButton( + child: Text(L10n.of(context).cancel), + onPressed: () => Navigator.of(context).pop(null), + ), + AdaptiveFlatButton( + child: Text(L10n.of(context).confirm), + onPressed: () => Navigator.of(context).pop(_permission), + ), + ]; + if (PlatformInfos.isCupertinoStyle) { + return CupertinoAlertDialog( + title: title, + content: content, + actions: buttons, + ); + } + return AlertDialog( + title: title, + content: content, + actions: buttons, + ); + } +} diff --git a/lib/components/user_bottom_sheet.dart b/lib/components/user_bottom_sheet.dart index d2081f5b..239bd94b 100644 --- a/lib/components/user_bottom_sheet.dart +++ b/lib/components/user_bottom_sheet.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/dialogs/permission_slider_dialog.dart'; import 'package:fluffychat/utils/app_route.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/views/chat.dart'; @@ -52,24 +53,14 @@ class UserBottomSheet extends StatelessWidget { Navigator.of(context).pop(); } break; - case 'admin': - if (await _askConfirmation()) { + case 'permission': + final newPermission = + await PermissionSliderDialog(initialPermission: user.powerLevel) + .show(context); + if (newPermission != null) { + if (newPermission == 100 && await _askConfirmation() == false) break; await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.setPower(100)); - Navigator.of(context).pop(); - } - break; - case 'moderator': - if (await _askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.setPower(50)); - Navigator.of(context).pop(); - } - break; - case 'user': - if (await _askConfirmation()) { - await SimpleDialogs(context) - .tryRequestWithLoadingDialog(user.setPower(0)); + .tryRequestWithLoadingDialog(user.setPower(newPermission)); Navigator.of(context).pop(); } break; @@ -119,38 +110,14 @@ class UserBottomSheet extends StatelessWidget { value: 'message'), ); } - if (user.canChangePowerLevel && - user.room.ownPowerLevel == 100 && - user.powerLevel != 100) { + if (user.canChangePowerLevel) { items.add( PopupMenuItem( child: _TextWithIcon( - L10n.of(context).makeAnAdmin, - Icons.arrow_upward, + L10n.of(context).setPermissionsLevel, + Icons.edit_attributes_outlined, ), - value: 'admin'), - ); - } - if (user.canChangePowerLevel && - user.room.ownPowerLevel >= 50 && - user.powerLevel != 50) { - items.add( - PopupMenuItem( - child: _TextWithIcon( - L10n.of(context).makeAModerator, - Icons.arrow_upward_outlined, - ), - value: 'moderator'), - ); - } - if (user.canChangePowerLevel && user.powerLevel != 0) { - items.add( - PopupMenuItem( - child: _TextWithIcon( - L10n.of(context).revokeAllPermissions, - Icons.arrow_downward_outlined, - ), - value: 'user'), + value: 'permission'), ); } if (user.canKick) { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 79b7536d..95939889 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1370,11 +1370,71 @@ "count": {} } }, + "editBlockedServers": "Edit blocked servers", + "@editBlockedServers": { + "type": "text", + "placeholders": {} + }, + "enableEncryption": "Enable encryption", + "@enableEncryption": { + "type": "text", + "placeholders": {} + }, + "replaceRoomWithNewerVersion": "Replace room with newer version", + "@replaceRoomWithNewerVersion": { + "type": "text", + "placeholders": {} + }, + "editRoomAvatar": "Edit room avatar", + "@editRoomAvatar": { + "type": "text", + "placeholders": {} + }, + "defaultPermissionLevel": "Default permission level", + "@defaultPermissionLevel": { + "type": "text", + "placeholders": {} + }, + "sendMessages": "Send messages", + "@sendMessages": { + "type": "text", + "placeholders": {} + }, + "configureChat": "Configure chat", + "@configureChat": { + "type": "text", + "placeholders": {} + }, + "participant": "Participant", + "@participant": { + "type": "text", + "placeholders": {} + }, "send": "Send", "@send": { "type": "text", "placeholders": {} }, + "whoCanPerformWhichAction": "Who can perform which action", + "@whoCanPerformWhichAction": { + "type": "text", + "placeholders": {} + }, + "editChatPermissions": "Edit chat permissions", + "@editChatPermissions": { + "type": "text", + "placeholders": {} + }, + "setCustomEmotes": "Set custom emotes", + "@setCustomEmotes": { + "type": "text", + "placeholders": {} + }, + "setPermissionsLevel": "Set permissions level", + "@setPermissionsLevel": { + "type": "text", + "placeholders": {} + }, "sendAMessage": "Send a message", "@sendAMessage": { "type": "text", diff --git a/lib/views/chat_details.dart b/lib/views/chat_details.dart index fa4b531f..83026493 100644 --- a/lib/views/chat_details.dart +++ b/lib/views/chat_details.dart @@ -1,6 +1,7 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/views/chat_permissions_settings.dart'; import 'package:flushbar/flushbar_helper.dart'; import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/matrix_api.dart'; @@ -316,6 +317,7 @@ class _ChatDetailsState extends State { child: Icon(Icons.insert_emoticon), ), title: Text(L10n.of(context).emoteSettings), + subtitle: Text(L10n.of(context).setCustomEmotes), onTap: () async { // okay, we need to test if there are any emote state events other than the default one // if so, we need to be directed to a selection screen for which pack we want to look at @@ -476,6 +478,24 @@ class _ChatDetailsState extends State { ), ], ), + ListTile( + title: Text(L10n.of(context).editChatPermissions), + subtitle: Text( + L10n.of(context).whoCanPerformWhichAction), + leading: CircleAvatar( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(Icons.edit_attributes_outlined), + ), + onTap: () => Navigator.of(context).push( + AppRoute.defaultRoute( + context, + ChatPermissionsSettingsView( + roomId: widget.room.id), + ), + ), + ), Divider(thickness: 1), ListTile( title: Text( diff --git a/lib/views/chat_permissions_settings.dart b/lib/views/chat_permissions_settings.dart new file mode 100644 index 00000000..7a3616e6 --- /dev/null +++ b/lib/views/chat_permissions_settings.dart @@ -0,0 +1,249 @@ +import 'dart:developer'; + +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/dialogs/permission_slider_dialog.dart'; +import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flushbar/flushbar_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:famedlysdk/famedlysdk.dart'; + +import 'chat_list.dart'; + +class ChatPermissionsSettingsView extends StatelessWidget { + final String roomId; + + const ChatPermissionsSettingsView({Key key, this.roomId}) : super(key: key); + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + firstScaffold: ChatList( + activeChat: roomId, + ), + secondScaffold: ChatPermissionsSettings(roomId: roomId), + primaryPage: FocusPage.SECOND, + ); + } +} + +class ChatPermissionsSettings extends StatelessWidget { + final String roomId; + + const ChatPermissionsSettings({Key key, @required this.roomId}) + : super(key: key); + + void _editPowerLevel(BuildContext context, String key, int currentLevel, + {String category}) async { + final room = Matrix.of(context).client.getRoomById(roomId); + if (!room.canSendEvent(EventTypes.RoomPowerLevels)) { + return FlushbarHelper.createError(message: L10n.of(context).noPermission) + .show(context); + } + final newLevel = + await PermissionSliderDialog(initialPermission: currentLevel) + .show(context); + if (newLevel == null) return; + final content = Map.from( + room.getState(EventTypes.RoomPowerLevels).content); + if (category != null) { + if (!content.containsKey(category)) { + content[category] = {}; + } + content[category][key] = newLevel; + } else { + content[key] = newLevel; + } + inspect(content); + await SimpleDialogs(context).tryRequestWithLoadingDialog( + room.client.sendState(room.id, EventTypes.RoomPowerLevels, content), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(L10n.of(context).editChatPermissions)), + body: StreamBuilder( + stream: Matrix.of(context).client.onSync.stream.where( + (e) => + (e?.rooms?.join?.containsKey(roomId) ?? false) && + (e.rooms.join[roomId]?.timeline?.events + ?.any((s) => s.type == EventTypes.RoomPowerLevels) ?? + false), + ), + builder: (context, _) { + final room = Matrix.of(context).client.getRoomById(roomId); + final powerLevelsContent = Map.from( + room.getState(EventTypes.RoomPowerLevels).content); + final powerLevels = Map.from(powerLevelsContent) + ..removeWhere((k, v) => !(v is int)); + final eventsPowerLevels = + Map.from(powerLevelsContent['events']) + ..removeWhere((k, v) => !(v is int)); + + return ListView( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (var entry in powerLevels.entries) + PermissionsListTile( + permissionKey: entry.key, + permission: entry.value, + onTap: () => + _editPowerLevel(context, entry.key, entry.value), + ), + Divider(thickness: 1), + ListTile( + title: Text( + L10n.of(context).notifications, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + Builder(builder: (context) { + final key = 'rooms'; + final int value = + powerLevelsContent.containsKey('notifications') + ? powerLevelsContent['notifications']['rooms'] ?? 0 + : 0; + return PermissionsListTile( + permissionKey: key, + permission: value, + category: 'notifications', + onTap: () => _editPowerLevel(context, key, value, + category: 'notifications'), + ); + }), + Divider(thickness: 1), + ListTile( + title: Text( + L10n.of(context).configureChat, + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + ), + ), + ), + if (eventsPowerLevels != null) + for (var entry in eventsPowerLevels.entries) + PermissionsListTile( + permissionKey: entry.key, + category: 'events', + permission: entry.value, + onTap: () => _editPowerLevel( + context, entry.key, entry.value, + category: 'events'), + ), + ], + ), + ], + ); + }, + ), + ); + } +} + +class PermissionsListTile extends StatelessWidget { + final String permissionKey; + final int permission; + final String category; + final void Function() onTap; + + const PermissionsListTile({ + Key key, + @required this.permissionKey, + @required this.permission, + this.category, + this.onTap, + }) : super(key: key); + + String getLocalizedPowerLevelString(BuildContext context) { + if (category == null) { + switch (permissionKey) { + case 'users_default': + return L10n.of(context).defaultPermissionLevel; + case 'events_default': + return L10n.of(context).sendMessages; + case 'state_default': + return L10n.of(context).configureChat; + case 'ban': + return L10n.of(context).banFromChat; + case 'kick': + return L10n.of(context).kickFromChat; + case 'redact': + return L10n.of(context).deleteMessage; + case 'invite': + return L10n.of(context).inviteContact; + } + } else if (category == 'notifications') { + switch (permissionKey) { + case 'rooms': + return L10n.of(context).notifications; + } + } else if (category == 'events') { + switch (permissionKey) { + case EventTypes.RoomName: + return L10n.of(context).changeTheNameOfTheGroup; + case EventTypes.RoomPowerLevels: + return L10n.of(context).editChatPermissions; + case EventTypes.HistoryVisibility: + return L10n.of(context).visibilityOfTheChatHistory; + case EventTypes.RoomCanonicalAlias: + return L10n.of(context).setInvitationLink; + case EventTypes.RoomAvatar: + return L10n.of(context).editRoomAvatar; + case EventTypes.RoomTombstone: + return L10n.of(context).replaceRoomWithNewerVersion; + case EventTypes.Encryption: + return L10n.of(context).enableEncryption; + case 'm.room.server_acl': + return L10n.of(context).editBlockedServers; + } + } + return permissionKey; + } + + @override + Widget build(BuildContext context) { + return ListTile( + onTap: onTap, + leading: CircleAvatar( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: Colors.grey, + child: Icon(Icons.edit_attributes_outlined), + ), + title: Text(getLocalizedPowerLevelString(context)), + subtitle: Row( + children: [ + Container( + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + color: Theme.of(context).secondaryHeaderColor, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text(permission.toString()), + ), + ), + SizedBox(width: 8), + Text(permission.toLocalizedPowerLevelString(context)), + ], + ), + ); + } +} + +extension on int { + String toLocalizedPowerLevelString(BuildContext context) { + return this == 100 + ? L10n.of(context).admin + : this >= 50 + ? L10n.of(context).moderator + : L10n.of(context).participant; + } +}