add power level groups
This commit is contained in:
parent
3a39f1c5ac
commit
6336764da0
4 changed files with 216 additions and 11 deletions
|
|
@ -218,7 +218,10 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
.run(() => setState(.powerLevels, content).onError(showError));
|
.run(() => setState(.powerLevels, content).onError(showError));
|
||||||
|
|
||||||
// Event types with their own level that don't have a setting of their own
|
// Event types with their own level that don't have a setting of their own
|
||||||
final listedEvents = shownEvents.map((event) => event.type.type).toISet();
|
final listedEvents = shownEvents
|
||||||
|
.addAll(PowerLevelGroup.values.expand((group) => group.events))
|
||||||
|
.map((event) => event.type.type)
|
||||||
|
.toISet();
|
||||||
final otherEvents = powerLevels.events.entries
|
final otherEvents = powerLevels.events.entries
|
||||||
.where((entry) => !listedEvents.contains(entry.key))
|
.where((entry) => !listedEvents.contains(entry.key))
|
||||||
.toIList()
|
.toIList()
|
||||||
|
|
@ -304,6 +307,76 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Widget tileOf(Setting setting) =>
|
||||||
|
setting.builder(setting.title, setting.description, setting.icon);
|
||||||
|
|
||||||
|
/// A setting for all of [group]'s levels at once, which expands to set each.
|
||||||
|
Setting groupSetting(PowerLevelGroup group) => .new(
|
||||||
|
title: group.title,
|
||||||
|
description: group.description,
|
||||||
|
icon: group.icon,
|
||||||
|
builder: (title, description, icon) => HookBuilder(
|
||||||
|
builder: (context) {
|
||||||
|
final expanded = useState(false);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [
|
||||||
|
PowerLevelTile(
|
||||||
|
leading: Icon(icon),
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
value: group.levelIn(powerLevels),
|
||||||
|
ownLevel: ownLevel,
|
||||||
|
// Changing it would change levels above the user's own
|
||||||
|
onChanged:
|
||||||
|
canSetPowerLevels &&
|
||||||
|
group.highestIn(powerLevels) <= ownLevel
|
||||||
|
? (level) =>
|
||||||
|
setPowerLevels(group.withLevel(powerLevels, level))
|
||||||
|
: null,
|
||||||
|
trailing: IconButton(
|
||||||
|
tooltip: expanded.value ? "Collapse" : "Set each",
|
||||||
|
icon: AnimatedRotation(
|
||||||
|
turns: expanded.value ? 0.5 : 0,
|
||||||
|
duration: Durations.medium2,
|
||||||
|
child: Icon(Icons.expand_more),
|
||||||
|
),
|
||||||
|
onPressed: () => expanded.value = !expanded.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedSize(
|
||||||
|
duration: Durations.medium2,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
alignment: .topCenter,
|
||||||
|
child: expanded.value
|
||||||
|
? Padding(
|
||||||
|
padding: .only(left: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
|
children: [
|
||||||
|
...group.permissions
|
||||||
|
.map(permissionSetting)
|
||||||
|
.map(tileOf),
|
||||||
|
...group.events
|
||||||
|
.map(
|
||||||
|
(event) => eventSetting(
|
||||||
|
event,
|
||||||
|
fallback: powerLevels.stateDefault,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map(tileOf),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(width: .infinity),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return .new({
|
return .new({
|
||||||
"Room": .new([
|
"Room": .new([
|
||||||
.new(
|
.new(
|
||||||
|
|
@ -668,6 +741,12 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
header(
|
||||||
|
"Groups",
|
||||||
|
"Power levels set together, which can each be set on their own.",
|
||||||
|
Icons.workspaces,
|
||||||
|
),
|
||||||
|
...PowerLevelGroup.values.map(groupSetting),
|
||||||
header(
|
header(
|
||||||
"Regular Events",
|
"Regular Events",
|
||||||
"Power levels needed to send messages and other timeline events.",
|
"Power levels needed to send messages and other timeline events.",
|
||||||
|
|
@ -682,14 +761,10 @@ class RoomSettingsSectionsController(final String roomId)
|
||||||
permissionSetting(.eventsDefault),
|
permissionSetting(.eventsDefault),
|
||||||
header(
|
header(
|
||||||
"State Events",
|
"State Events",
|
||||||
"Power levels needed to manage members and change the room's state.",
|
"Power levels needed to change the room's state.",
|
||||||
Icons.settings,
|
Icons.settings,
|
||||||
),
|
),
|
||||||
eventSetting(.pinnedEvents, fallback: powerLevels.stateDefault),
|
|
||||||
permissionSetting(.invite),
|
permissionSetting(.invite),
|
||||||
permissionSetting(.kick),
|
|
||||||
permissionSetting(.ban),
|
|
||||||
permissionSetting(.redact),
|
|
||||||
permissionSetting(.stateDefault),
|
permissionSetting(.stateDefault),
|
||||||
header(
|
header(
|
||||||
"Other Events",
|
"Other Events",
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ enum EventType(
|
||||||
avatar("m.room.avatar", AvatarContent.fromJson),
|
avatar("m.room.avatar", AvatarContent.fromJson),
|
||||||
topic("m.room.topic", TopicContent.fromJson),
|
topic("m.room.topic", TopicContent.fromJson),
|
||||||
name("m.room.name", NameContent.fromJson),
|
name("m.room.name", NameContent.fromJson),
|
||||||
|
tombstone("m.room.tombstone", Content.fromJson),
|
||||||
reaction("m.reaction", ReactionContent.fromJson),
|
reaction("m.reaction", ReactionContent.fromJson),
|
||||||
pinnedEvents("m.room.pinned_events", PinnedEventsContent.fromJson),
|
pinnedEvents("m.room.pinned_events", PinnedEventsContent.fromJson),
|
||||||
message("m.room.message", MessageContent.fromJson),
|
message("m.room.message", MessageContent.fromJson),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import "dart:math";
|
||||||
|
|
||||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||||
import "package:material_ui/material_ui.dart";
|
import "package:material_ui/material_ui.dart";
|
||||||
import "package:nexus/models/content/content.dart";
|
import "package:nexus/models/content/content.dart";
|
||||||
|
|
@ -113,14 +115,133 @@ enum PowerLevelEvent(
|
||||||
"Pin Messages",
|
"Pin Messages",
|
||||||
"The power level needed to pin messages.",
|
"The power level needed to pin messages.",
|
||||||
Icons.push_pin,
|
Icons.push_pin,
|
||||||
|
),
|
||||||
|
name(
|
||||||
|
.name,
|
||||||
|
"Change Name",
|
||||||
|
"The power level needed to change the room's name.",
|
||||||
|
Icons.title,
|
||||||
|
),
|
||||||
|
topic(
|
||||||
|
.topic,
|
||||||
|
"Change Topic",
|
||||||
|
"The power level needed to change the room's topic.",
|
||||||
|
Icons.description,
|
||||||
|
),
|
||||||
|
avatar(
|
||||||
|
.avatar,
|
||||||
|
"Change Avatar",
|
||||||
|
"The power level needed to change the room's avatar.",
|
||||||
|
Icons.image,
|
||||||
|
),
|
||||||
|
canonicalAlias(
|
||||||
|
.canonicalAlias,
|
||||||
|
"Change Aliases",
|
||||||
|
"The power level needed to change the room's canonical alias and published aliases.",
|
||||||
|
Icons.numbers,
|
||||||
|
),
|
||||||
|
powerLevels(
|
||||||
|
.powerLevels,
|
||||||
|
"Change Power Levels",
|
||||||
|
"The power level needed to change power levels, including these.",
|
||||||
|
Icons.admin_panel_settings,
|
||||||
|
),
|
||||||
|
joinRules(
|
||||||
|
.joinRules,
|
||||||
|
"Change Join Rule",
|
||||||
|
"The power level needed to change who can join the room.",
|
||||||
|
Icons.login,
|
||||||
|
),
|
||||||
|
historyVisibility(
|
||||||
|
.historyVisibility,
|
||||||
|
"Change History Visibility",
|
||||||
|
"The power level needed to change who can read the room's history.",
|
||||||
|
Icons.history,
|
||||||
|
),
|
||||||
|
serverACL(
|
||||||
|
.serverACL,
|
||||||
|
"Change Server ACL",
|
||||||
|
"The power level needed to change which servers can take part in the room.",
|
||||||
|
Icons.dns,
|
||||||
|
),
|
||||||
|
encryption(
|
||||||
|
.encryption,
|
||||||
|
"Enable Encryption",
|
||||||
|
"The power level needed to enable encryption, which can't be turned off.",
|
||||||
|
Icons.lock,
|
||||||
|
),
|
||||||
|
tombstone(
|
||||||
|
.tombstone,
|
||||||
|
"Upgrade Room",
|
||||||
|
"The power level needed to replace the room with an upgraded one.",
|
||||||
|
Icons.upgrade,
|
||||||
);
|
);
|
||||||
|
|
||||||
static IList<PowerLevelEvent> shownIn({required bool encrypted}) => .new([
|
static IList<PowerLevelEvent> shownIn({required bool encrypted}) => .new([
|
||||||
if (encrypted) .encrypted else ...[.message, .sticker],
|
if (encrypted) .encrypted else ...[.message, .sticker],
|
||||||
.reaction,
|
.reaction,
|
||||||
.pinnedEvents,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
static PowerLevelEvent? of(String type) =>
|
static PowerLevelEvent? of(String type) =>
|
||||||
values.where((event) => event.type.type == type).firstOrNull;
|
values.where((event) => event.type.type == type).firstOrNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Power levels set together, like everything moderators need, to save
|
||||||
|
/// setting each one. Each can still be set on its own, by expanding it.
|
||||||
|
enum PowerLevelGroup(final String title, final IconData icon) {
|
||||||
|
moderation("Moderation", Icons.shield),
|
||||||
|
roomDetails("Room Details", Icons.edit),
|
||||||
|
administration("Administration", Icons.admin_panel_settings);
|
||||||
|
|
||||||
|
IList<PowerLevelPermission> get permissions => .new(switch (this) {
|
||||||
|
.moderation => [.kick, .ban, .redact, .notifyRoom],
|
||||||
|
.roomDetails || .administration => [],
|
||||||
|
});
|
||||||
|
|
||||||
|
IList<PowerLevelEvent> get events => .new(switch (this) {
|
||||||
|
.moderation => [],
|
||||||
|
.roomDetails => [.name, .topic, .avatar, .canonicalAlias, .pinnedEvents],
|
||||||
|
.administration => [
|
||||||
|
.powerLevels,
|
||||||
|
.joinRules,
|
||||||
|
.historyVisibility,
|
||||||
|
.serverACL,
|
||||||
|
.encryption,
|
||||||
|
.tombstone,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
String get description => [
|
||||||
|
...permissions.map((permission) => permission.title),
|
||||||
|
...events.map((event) => event.title),
|
||||||
|
].join(", ");
|
||||||
|
|
||||||
|
int? levelIn(PowerLevelsContent content) {
|
||||||
|
final levels = {
|
||||||
|
...permissions.map((permission) => permission.levelIn(content)),
|
||||||
|
...events.map(
|
||||||
|
(event) => content.events[event.type.type] ?? content.stateDefault,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
return levels.length == 1 ? levels.single : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int highestIn(PowerLevelsContent content) => [
|
||||||
|
...permissions.map((permission) => permission.levelIn(content)),
|
||||||
|
...events.map(
|
||||||
|
(event) => content.events[event.type.type] ?? content.stateDefault,
|
||||||
|
),
|
||||||
|
].reduce(max);
|
||||||
|
|
||||||
|
PowerLevelsContent withLevel(PowerLevelsContent content, int level) =>
|
||||||
|
permissions
|
||||||
|
.fold(
|
||||||
|
content,
|
||||||
|
(content, permission) => permission.withLevel(content, level),
|
||||||
|
)
|
||||||
|
.copyWith(
|
||||||
|
events: content.events.addEntries(
|
||||||
|
events.map((event) => MapEntry(event.type.type, level)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ class const PowerLevelTile({
|
||||||
required final Widget leading,
|
required final Widget leading,
|
||||||
required final String title,
|
required final String title,
|
||||||
required final String? description,
|
required final String? description,
|
||||||
required final int value,
|
|
||||||
|
/// Null for several levels that differ, shown as custom.
|
||||||
|
required final int? value,
|
||||||
|
|
||||||
/// The user's own power level, which they can't set or change levels above.
|
/// The user's own power level, which they can't set or change levels above.
|
||||||
required final double ownLevel,
|
required final double ownLevel,
|
||||||
|
|
@ -22,11 +24,14 @@ class const PowerLevelTile({
|
||||||
|
|
||||||
/// If given, asked before removing the level.
|
/// If given, asked before removing the level.
|
||||||
final String? removeConfirmation,
|
final String? removeConfirmation,
|
||||||
|
|
||||||
|
/// Shown after the level, like a button to expand what it sets.
|
||||||
|
final Widget? trailing,
|
||||||
super.key,
|
super.key,
|
||||||
}) extends StatelessWidget {
|
}) extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final enabled = onChanged != null && value <= ownLevel;
|
final enabled = onChanged != null && (value ?? 0) <= ownLevel;
|
||||||
|
|
||||||
Future<void> remove() async {
|
Future<void> remove() async {
|
||||||
if (removeConfirmation case final confirmation?) {
|
if (removeConfirmation case final confirmation?) {
|
||||||
|
|
@ -65,7 +70,7 @@ class const PowerLevelTile({
|
||||||
children: [
|
children: [
|
||||||
Chip(
|
Chip(
|
||||||
label: Text(
|
label: Text(
|
||||||
"$value",
|
value?.toString() ?? "Custom",
|
||||||
style: enabled ? null : .new(color: Colors.grey),
|
style: enabled ? null : .new(color: Colors.grey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -75,12 +80,15 @@ class const PowerLevelTile({
|
||||||
icon: Icon(Icons.delete),
|
icon: Icon(Icons.delete),
|
||||||
onPressed: enabled ? remove : null,
|
onPressed: enabled ? remove : null,
|
||||||
),
|
),
|
||||||
|
?trailing,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () => context.showAdaptiveSheet(
|
onTap: () => context.showAdaptiveSheet(
|
||||||
(context) => HookBuilder(
|
(context) => HookBuilder(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final controller = useTextEditingController(text: "$value");
|
final controller = useTextEditingController(
|
||||||
|
text: value?.toString(),
|
||||||
|
);
|
||||||
final level = PowerLevelField.levelFrom(
|
final level = PowerLevelField.levelFrom(
|
||||||
useValueListenable(controller).text,
|
useValueListenable(controller).text,
|
||||||
ownLevel,
|
ownLevel,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue