diff --git a/lib/controllers/power_level_controller.dart b/lib/controllers/power_level_controller.dart new file mode 100644 index 0000000..db57d4f --- /dev/null +++ b/lib/controllers/power_level_controller.dart @@ -0,0 +1,48 @@ +import "package:collection/collection.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/controllers/client_state_controller.dart"; +import "package:nexus/controllers/selected_room_controller.dart"; +import "package:nexus/models/configs/power_level_config.dart"; +import "package:nexus/models/requests/membership_action.dart"; + +class PowerLevelController extends Notifier { + final PowerLevelConfig config; + PowerLevelController(this.config); + + @override + bool build() { + final room = ref.watch(SelectedRoomController.provider); + final event = room?.events.firstWhereOrNull( + (event) => event.rowId == room.state["m.room.power_levels"]?[""], + ); + final user = ref.watch(ClientStateController.provider)?.userId; + if (event == null || user == null) return false; + + final users = (event.content["users"] as Map? ?? {}); + final events = (event.content["events"] as Map? ?? {}); + + final userLevel = users.containsKey(user) + ? (users[user] as int) + : (event.content["users_default"] as int? ?? 0); + + final requiredLevel = switch (config.action) { + MembershipAction.ban || + MembershipAction.unban => (event.content["ban"] as int? ?? 50), + MembershipAction.kick => (event.content["kick"] as int? ?? 50), + MembershipAction.invite => (event.content["invite"] as int? ?? 0), + null => + events.containsKey(config.eventType) + ? (events[config.eventType] as int) + : (config.isStateEvent + ? (event.content["state_default"] as int? ?? 50) + : (event.content["events_default"] as int? ?? 0)), + }; + + return userLevel >= requiredLevel; + } + + static final provider = NotifierProvider.autoDispose + .family( + PowerLevelController.new, + ); +} diff --git a/lib/models/configs/power_level_config.dart b/lib/models/configs/power_level_config.dart new file mode 100644 index 0000000..c051fed --- /dev/null +++ b/lib/models/configs/power_level_config.dart @@ -0,0 +1,16 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:nexus/models/requests/membership_action.dart"; +part "power_level_config.freezed.dart"; +part "power_level_config.g.dart"; + +@freezed +abstract class PowerLevelConfig with _$PowerLevelConfig { + const factory PowerLevelConfig({ + @Default(false) bool isStateEvent, + required String eventType, + MembershipAction? action, + }) = _PowerLevelConfig; + + factory PowerLevelConfig.fromJson(Map json) => + _$PowerLevelConfigFromJson(json); +} diff --git a/lib/models/requests/membership_action.dart b/lib/models/requests/membership_action.dart new file mode 100644 index 0000000..d852164 --- /dev/null +++ b/lib/models/requests/membership_action.dart @@ -0,0 +1,4 @@ +import "package:freezed_annotation/freezed_annotation.dart"; + +@JsonEnum() +enum MembershipAction { ban, kick, unban, invite } diff --git a/lib/models/requests/set_membership_request.dart b/lib/models/requests/set_membership_request.dart index ae101e8..dd0e1f2 100644 --- a/lib/models/requests/set_membership_request.dart +++ b/lib/models/requests/set_membership_request.dart @@ -1,4 +1,5 @@ import "package:freezed_annotation/freezed_annotation.dart"; +import "package:nexus/models/requests/membership_action.dart"; part "set_membership_request.freezed.dart"; part "set_membership_request.g.dart"; @@ -16,6 +17,3 @@ abstract class SetMembershipRequest with _$SetMembershipRequest { factory SetMembershipRequest.fromJson(Map json) => _$SetMembershipRequestFromJson(json); } - -@JsonEnum() -enum MembershipAction { ban, kick, unban, invite } diff --git a/lib/widgets/chat_page/composer/chat_box.dart b/lib/widgets/chat_page/composer/chat_box.dart index d1c9c61..3c2057e 100644 --- a/lib/widgets/chat_page/composer/chat_box.dart +++ b/lib/widgets/chat_page/composer/chat_box.dart @@ -5,6 +5,8 @@ import "package:flutter_chat_core/flutter_chat_core.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:fluttertagger/fluttertagger.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:nexus/controllers/power_level_controller.dart"; +import "package:nexus/models/configs/power_level_config.dart"; import "package:nexus/models/relation_type.dart"; import "package:nexus/widgets/chat_page/composer/mention_overlay.dart"; import "package:nexus/widgets/chat_page/composer/relation_preview.dart"; @@ -42,6 +44,7 @@ class ChatBox extends HookConsumerWidget { } void send() { + if (controller.value.text.isEmpty) return; onSend( controller.value.formattedText, shouldMention: shouldMention.value, @@ -69,6 +72,12 @@ class ChatBox extends HookConsumerWidget { fontWeight: FontWeight.bold, ); + final canSendMessages = ref.watch( + PowerLevelController.provider( + PowerLevelConfig(eventType: "m.room.message"), + ), + ); + return Positioned( bottom: 0, left: 0, @@ -95,6 +104,7 @@ class ChatBox extends HookConsumerWidget { children: [ PopupMenuButton( tooltip: "Add media", + enabled: canSendMessages, itemBuilder: (context) => [ PopupMenuItem( child: ListTile( @@ -136,12 +146,11 @@ class ChatBox extends HookConsumerWidget { }, triggerCharacterAndStyles: {"@": style, "#": style}, builder: (context, key) => TextFormField( - // enabled: room.canSendDefaultMessages, + enabled: canSendMessages, maxLines: 12, minLines: 1, decoration: InputDecoration( - hintText: - true // TODO: room.canSendDefaultMessages + hintText: canSendMessages ? "Your message here..." : "You don't have permission to send messages in this room...", border: InputBorder.none, @@ -156,7 +165,7 @@ class ChatBox extends HookConsumerWidget { ), ), IconButton( - onPressed: send, + onPressed: !canSendMessages ? null : send, // onPressed: room.canSendDefaultMessages ? send : null, icon: Icon(Icons.send), tooltip: "Send message", diff --git a/lib/widgets/chat_page/user_popover.dart b/lib/widgets/chat_page/user_popover.dart index 2baae22..2f76d9f 100644 --- a/lib/widgets/chat_page/user_popover.dart +++ b/lib/widgets/chat_page/user_popover.dart @@ -4,11 +4,14 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:intl/intl.dart"; import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/client_state_controller.dart"; +import "package:nexus/controllers/power_level_controller.dart"; import "package:nexus/controllers/profile_controller.dart"; import "package:nexus/controllers/selected_room_controller.dart"; import "package:nexus/helpers/extensions/better_when.dart"; +import "package:nexus/models/configs/power_level_config.dart"; import "package:nexus/models/membership.dart"; import "package:nexus/models/membership_status.dart"; +import "package:nexus/models/requests/membership_action.dart"; import "package:nexus/models/requests/set_membership_request.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/main.dart"; @@ -150,7 +153,17 @@ class UserPopover extends ConsumerWidget { runSpacing: 8, children: [ FilledButton.icon(onPressed: null, label: Text("Message")), - if (member.status == MembershipStatus.join || + + if (ref.watch( + PowerLevelController.provider( + PowerLevelConfig( + eventType: "m.room.member", + action: MembershipAction.kick, + isStateEvent: true, + ), + ), + ) && + member.status == MembershipStatus.join || member.status == MembershipStatus.invite) FilledButton.icon( onPressed: () => showMembershipDialog(MembershipAction.kick), @@ -164,24 +177,33 @@ class UserPopover extends ConsumerWidget { ), ), ), - ElevatedButton.icon( - onPressed: () => showMembershipDialog( - member.status == MembershipStatus.ban - ? MembershipAction.unban - : MembershipAction.ban, - ), - label: Text( - member.status == MembershipStatus.ban ? "Unban" : "Ban", - ), - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - theme.colorScheme.errorContainer, - ), - foregroundColor: WidgetStatePropertyAll( - theme.colorScheme.onErrorContainer, + if (ref.watch( + PowerLevelController.provider( + PowerLevelConfig( + eventType: "m.room.member", + action: MembershipAction.ban, + isStateEvent: true, + ), + ), + )) + ElevatedButton.icon( + onPressed: () => showMembershipDialog( + member.status == MembershipStatus.ban + ? MembershipAction.unban + : MembershipAction.ban, + ), + label: Text( + member.status == MembershipStatus.ban ? "Unban" : "Ban", + ), + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + theme.colorScheme.errorContainer, + ), + foregroundColor: WidgetStatePropertyAll( + theme.colorScheme.onErrorContainer, + ), ), ), - ), ], ), ],