Add powerlevel checks

This commit is contained in:
Henry Hiles 2026-04-04 17:44:55 -04:00
commit f38715c8ef
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
6 changed files with 119 additions and 22 deletions

View file

@ -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<bool> {
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<String, dynamic>? ?? {});
final events = (event.content["events"] as Map<String, dynamic>? ?? {});
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, bool, PowerLevelConfig>(
PowerLevelController.new,
);
}

View file

@ -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<String, Object?> json) =>
_$PowerLevelConfigFromJson(json);
}

View file

@ -0,0 +1,4 @@
import "package:freezed_annotation/freezed_annotation.dart";
@JsonEnum()
enum MembershipAction { ban, kick, unban, invite }

View file

@ -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<String, Object?> json) =>
_$SetMembershipRequestFromJson(json);
}
@JsonEnum()
enum MembershipAction { ban, kick, unban, invite }

View file

@ -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",

View file

@ -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,
),
),
),
),
],
),
],