feat: Add chat permissions settings

This commit is contained in:
Christian Pauly 2020-12-05 13:03:57 +01:00
parent e30772eefc
commit bf4b439572
7 changed files with 466 additions and 81 deletions

View File

@ -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,
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../avatar.dart'; import '../avatar.dart';
import 'adaptive_flat_button.dart';
import 'simple_dialogs.dart'; import 'simple_dialogs.dart';
import '../../utils/string_color.dart'; import '../../utils/string_color.dart';
@ -121,14 +122,14 @@ class _KeyVerificationPageState extends State<KeyVerificationDialog> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
), ),
); );
buttons.add(_AdaptiveFlatButton( buttons.add(AdaptiveFlatButton(
child: Text(L10n.of(context).submit), child: Text(L10n.of(context).submit),
onPressed: () { onPressed: () {
input = textEditingController.text; input = textEditingController.text;
checkInput(); checkInput();
}, },
)); ));
buttons.add(_AdaptiveFlatButton( buttons.add(AdaptiveFlatButton(
child: Text(L10n.of(context).skip), child: Text(L10n.of(context).skip),
onPressed: () => widget.request.openSSSS(skip: true), onPressed: () => widget.request.openSSSS(skip: true),
)); ));
@ -140,11 +141,11 @@ class _KeyVerificationPageState extends State<KeyVerificationDialog> {
style: TextStyle(fontSize: 20)), style: TextStyle(fontSize: 20)),
margin: EdgeInsets.only(left: 8.0, right: 8.0), margin: EdgeInsets.only(left: 8.0, right: 8.0),
); );
buttons.add(_AdaptiveFlatButton( buttons.add(AdaptiveFlatButton(
child: Text(L10n.of(context).accept), child: Text(L10n.of(context).accept),
onPressed: () => widget.request.acceptVerification(), onPressed: () => widget.request.acceptVerification(),
)); ));
buttons.add(_AdaptiveFlatButton( buttons.add(AdaptiveFlatButton(
child: Text(L10n.of(context).reject), child: Text(L10n.of(context).reject),
onPressed: () { onPressed: () {
widget.request.rejectVerification().then((_) { widget.request.rejectVerification().then((_) {
@ -204,11 +205,11 @@ class _KeyVerificationPageState extends State<KeyVerificationDialog> {
], ],
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
); );
buttons.add(_AdaptiveFlatButton( buttons.add(AdaptiveFlatButton(
child: Text(L10n.of(context).theyMatch), child: Text(L10n.of(context).theyMatch),
onPressed: () => widget.request.acceptSas(), onPressed: () => widget.request.acceptSas(),
)); ));
buttons.add(_AdaptiveFlatButton( buttons.add(AdaptiveFlatButton(
textColor: Colors.red, textColor: Colors.red,
child: Text(L10n.of(context).theyDontMatch), child: Text(L10n.of(context).theyDontMatch),
onPressed: () => widget.request.rejectSas(), onPressed: () => widget.request.rejectSas(),
@ -244,7 +245,7 @@ class _KeyVerificationPageState extends State<KeyVerificationDialog> {
], ],
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
); );
buttons.add(_AdaptiveFlatButton( buttons.add(AdaptiveFlatButton(
child: Text(L10n.of(context).close), child: Text(L10n.of(context).close),
onPressed: () => Navigator.of(context).pop(), 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,
);
}
}

View File

@ -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<int> show(BuildContext context) => PlatformInfos.isCupertinoStyle
? showCupertinoDialog<int>(context: context, builder: (context) => this)
: showDialog<int>(context: context, builder: (context) => this);
final int initialPermission;
@override
_PermissionSliderDialogState createState() => _PermissionSliderDialogState();
}
class _PermissionSliderDialogState extends State<PermissionSliderDialog> {
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<int>(null),
),
AdaptiveFlatButton(
child: Text(L10n.of(context).confirm),
onPressed: () => Navigator.of(context).pop<int>(_permission),
),
];
if (PlatformInfos.isCupertinoStyle) {
return CupertinoAlertDialog(
title: title,
content: content,
actions: buttons,
);
}
return AlertDialog(
title: title,
content: content,
actions: buttons,
);
}
}

View File

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/components/adaptive_page_layout.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/app_route.dart';
import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/views/chat.dart'; import 'package:fluffychat/views/chat.dart';
@ -52,24 +53,14 @@ class UserBottomSheet extends StatelessWidget {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
break; break;
case 'admin': case 'permission':
if (await _askConfirmation()) { final newPermission =
await PermissionSliderDialog(initialPermission: user.powerLevel)
.show(context);
if (newPermission != null) {
if (newPermission == 100 && await _askConfirmation() == false) break;
await SimpleDialogs(context) await SimpleDialogs(context)
.tryRequestWithLoadingDialog(user.setPower(100)); .tryRequestWithLoadingDialog(user.setPower(newPermission));
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));
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
break; break;
@ -119,38 +110,14 @@ class UserBottomSheet extends StatelessWidget {
value: 'message'), value: 'message'),
); );
} }
if (user.canChangePowerLevel && if (user.canChangePowerLevel) {
user.room.ownPowerLevel == 100 &&
user.powerLevel != 100) {
items.add( items.add(
PopupMenuItem( PopupMenuItem(
child: _TextWithIcon( child: _TextWithIcon(
L10n.of(context).makeAnAdmin, L10n.of(context).setPermissionsLevel,
Icons.arrow_upward, Icons.edit_attributes_outlined,
), ),
value: 'admin'), value: 'permission'),
);
}
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'),
); );
} }
if (user.canKick) { if (user.canKick) {

View File

@ -1370,11 +1370,71 @@
"count": {} "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": "Send",
"@send": { "@send": {
"type": "text", "type": "text",
"placeholders": {} "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": "Send a message",
"@sendAMessage": { "@sendAMessage": {
"type": "text", "type": "text",

View File

@ -1,6 +1,7 @@
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/views/chat_permissions_settings.dart';
import 'package:flushbar/flushbar_helper.dart'; import 'package:flushbar/flushbar_helper.dart';
import 'package:famedlysdk/famedlysdk.dart'; import 'package:famedlysdk/famedlysdk.dart';
import 'package:famedlysdk/matrix_api.dart'; import 'package:famedlysdk/matrix_api.dart';
@ -316,6 +317,7 @@ class _ChatDetailsState extends State<ChatDetails> {
child: Icon(Icons.insert_emoticon), child: Icon(Icons.insert_emoticon),
), ),
title: Text(L10n.of(context).emoteSettings), title: Text(L10n.of(context).emoteSettings),
subtitle: Text(L10n.of(context).setCustomEmotes),
onTap: () async { onTap: () async {
// okay, we need to test if there are any emote state events other than the default one // 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 // 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<ChatDetails> {
), ),
], ],
), ),
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), Divider(thickness: 1),
ListTile( ListTile(
title: Text( title: Text(

View File

@ -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<String, dynamic>.from(
room.getState(EventTypes.RoomPowerLevels).content);
if (category != null) {
if (!content.containsKey(category)) {
content[category] = <String, dynamic>{};
}
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<String, dynamic>.from(
room.getState(EventTypes.RoomPowerLevels).content);
final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
..removeWhere((k, v) => !(v is int));
final eventsPowerLevels =
Map<String, dynamic>.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;
}
}