chore: Adjust power level UX

This commit is contained in:
Christian Kußowski 2026-02-25 13:12:47 +01:00
commit 47934a3378
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
4 changed files with 186 additions and 106 deletions

View file

@ -3126,5 +3126,12 @@
"theProcessWasCanceled": "The process was canceled.", "theProcessWasCanceled": "The process was canceled.",
"join": "Join", "join": "Join",
"searchOrEnterHomeserverAddress": "Search or enter homeserver address", "searchOrEnterHomeserverAddress": "Search or enter homeserver address",
"matrixId": "Matrix ID" "matrixId": "Matrix ID",
"setPowerLevel": "Set power level",
"makeModerator": "Make moderator",
"makeAdmin": "Make admin",
"removeModeratorRights": "Remove moderator rights",
"removeAdminRights": "Remove admin rights",
"powerLevel": "Power level",
"setPowerLevelDescription": "Power levels define what a member is allowed to do in this room and usually range between 0 and 100."
} }

View file

@ -210,6 +210,7 @@ class Message extends StatelessWidget {
singleSelected && event.room.canSendDefaultMessages; singleSelected && event.room.canSendDefaultMessages;
final enterThread = this.enterThread; final enterThread = this.enterThread;
final sender = event.senderFromMemoryOrFallback;
return Center( return Center(
child: Swipeable( child: Swipeable(
@ -358,9 +359,7 @@ class Message extends StatelessWidget {
FutureBuilder<User?>( FutureBuilder<User?>(
future: event.fetchSenderUser(), future: event.fetchSenderUser(),
builder: (context, snapshot) { builder: (context, snapshot) {
final user = final user = snapshot.data ?? sender;
snapshot.data ??
event.senderFromMemoryOrFallback;
return Avatar( return Avatar(
mxContent: user.avatarUrl, mxContent: user.avatarUrl,
name: user.calcDisplayname(), name: user.calcDisplayname(),
@ -392,52 +391,78 @@ class Message extends StatelessWidget {
ownMessage || ownMessage ||
event.room.isDirectChat event.room.isDirectChat
? const SizedBox(height: 12) ? const SizedBox(height: 12)
: FutureBuilder<User?>( : Row(
future: event children: [
.fetchSenderUser(), if (sender.powerLevel >=
builder: (context, snapshot) { 50)
final displayname = Padding(
snapshot.data padding:
?.calcDisplayname() ?? const EdgeInsets.only(
event right: 2.0,
.senderFromMemoryOrFallback ),
.calcDisplayname(); child: Icon(
return Text( sender.powerLevel >=
displayname, 100
style: TextStyle( ? Icons
fontSize: 11, .admin_panel_settings
fontWeight: : Icons
FontWeight.bold, .add_moderator_outlined,
color: size: 14,
(theme.brightness == color: theme
Brightness .colorScheme
.light .onPrimaryContainer,
? displayname ),
.color
: displayname
.lightColorText),
shadows:
!wallpaperMode
? null
: [
const Shadow(
offset:
Offset(
0.0,
0.0,
),
blurRadius:
3,
color: Colors
.black,
),
],
), ),
maxLines: 1, Expanded(
overflow: TextOverflow child: FutureBuilder<User?>(
.ellipsis, future: event
); .fetchSenderUser(),
}, builder: (context, snapshot) {
final displayname =
snapshot.data
?.calcDisplayname() ??
sender
.calcDisplayname();
return Text(
displayname,
style: TextStyle(
fontSize: 11,
fontWeight:
FontWeight
.bold,
color:
(theme.brightness ==
Brightness
.light
? displayname
.color
: displayname
.lightColorText),
shadows:
!wallpaperMode
? null
: [
const Shadow(
offset: Offset(
0.0,
0.0,
),
blurRadius:
3,
color:
Colors.black,
),
],
),
maxLines: 1,
overflow:
TextOverflow
.ellipsis,
);
},
),
),
],
), ),
), ),
Container( Container(

View file

@ -43,13 +43,7 @@ Future<void> showMemberActionsPopupMenu({
child: Row( child: Row(
spacing: 12.0, spacing: 12.0,
children: [ children: [
Avatar( Avatar(name: displayname, size: 30, mxContent: user.avatarUrl),
name: displayname,
size: 30,
mxContent: user.avatarUrl,
presenceUserId: user.id,
presenceBackgroundColor: theme.colorScheme.surfaceContainer,
),
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200), constraints: const BoxConstraints(maxWidth: 200),
child: Text( child: Text(
@ -83,31 +77,71 @@ Future<void> showMemberActionsPopupMenu({
], ],
), ),
), ),
PopupMenuItem( if (user.canChangeUserPowerLevel) ...[
enabled: user.room.canChangePowerLevel && user.canChangeUserPowerLevel, if (user.powerLevel < 100)
value: _MemberActions.setRole, PopupMenuItem(
child: Row( value: _MemberActions.makeAdmin,
children: [ child: Row(
const Icon(Icons.admin_panel_settings_outlined),
const SizedBox(width: 18),
Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [ children: [
Text(L10n.of(context).chatPermissions), const Icon(Icons.admin_panel_settings_outlined),
Text( const SizedBox(width: 18),
user.powerLevel < 50 Text(L10n.of(context).makeAdmin),
? L10n.of(context).userLevel(user.powerLevel)
: user.powerLevel < 100
? L10n.of(context).moderatorLevel(user.powerLevel)
: L10n.of(context).adminLevel(user.powerLevel),
style: const TextStyle(fontSize: 10),
),
], ],
), ),
], ),
if (user.powerLevel < 50)
PopupMenuItem(
value: _MemberActions.makeModerator,
child: Row(
children: [
const Icon(Icons.add_moderator_outlined),
const SizedBox(width: 18),
Text(L10n.of(context).makeModerator),
],
),
),
if (user.powerLevel >= 100)
PopupMenuItem(
value: _MemberActions.removeAdmin,
child: Row(
children: [
const Icon(Icons.remove_moderator_outlined),
const SizedBox(width: 18),
Text(L10n.of(context).removeAdminRights),
],
),
)
else if (user.powerLevel >= 50)
PopupMenuItem(
value: _MemberActions.removeModerator,
child: Row(
children: [
const Icon(Icons.remove_moderator_outlined),
const SizedBox(width: 18),
Text(L10n.of(context).removeModeratorRights),
],
),
),
],
if (user.canChangeUserPowerLevel ||
!{0, 50, 100}.contains(user.powerLevel))
PopupMenuItem(
value: _MemberActions.setPowerLevel,
enabled: user.canChangeUserPowerLevel,
child: Row(
children: [
const Icon(Icons.manage_accounts_outlined),
const SizedBox(width: 18),
Text(
user.canChangeUserPowerLevel
? L10n.of(context).setPowerLevel
: L10n.of(context).powerLevel,
),
if (!{0, 50, 100}.contains(user.powerLevel))
Text(' (${user.powerLevel})'),
],
),
), ),
),
if (user.canKick) if (user.canKick)
PopupMenuItem( PopupMenuItem(
value: _MemberActions.kick, value: _MemberActions.kick,
@ -179,7 +213,7 @@ Future<void> showMemberActionsPopupMenu({
case _MemberActions.mention: case _MemberActions.mention:
onMention?.call(); onMention?.call();
return; return;
case _MemberActions.setRole: case _MemberActions.setPowerLevel:
final power = await showPermissionChooser( final power = await showPermissionChooser(
context, context,
currentLevel: user.powerLevel, currentLevel: user.powerLevel,
@ -280,13 +314,48 @@ Future<void> showMemberActionsPopupMenu({
future: () => user.unban(), future: () => user.unban(),
); );
} }
case _MemberActions.makeAdmin:
if (user.room.ownPowerLevel <= 100) {
final consent = await showOkCancelAlertDialog(
context: context,
title: L10n.of(context).areYouSure,
message: L10n.of(context).makeAdminDescription,
);
if (consent != OkCancelResult.ok) return;
if (!context.mounted) return;
}
await showFutureLoadingDialog(
context: context,
future: () => user.setPower(100),
);
case _MemberActions.makeModerator:
await showFutureLoadingDialog(
context: context,
future: () => user.setPower(50),
);
case _MemberActions.removeAdmin:
case _MemberActions.removeModerator:
final defaultUserLevel =
user.room
.getState(EventTypes.RoomPowerLevels)
?.content
.tryGet<int>('users_default') ??
0;
await showFutureLoadingDialog(
context: context,
future: () => user.setPower(defaultUserLevel),
);
} }
} }
enum _MemberActions { enum _MemberActions {
info, info,
mention, mention,
setRole, setPowerLevel,
makeAdmin,
makeModerator,
removeAdmin,
removeModerator,
kick, kick,
ban, ban,
approve, approve,

View file

@ -9,7 +9,7 @@ Future<int?> showPermissionChooser(
int currentLevel = 0, int currentLevel = 0,
int maxLevel = 100, int maxLevel = 100,
}) async { }) async {
final controller = TextEditingController(); final controller = TextEditingController(text: currentLevel.toString());
final error = ValueNotifier<String?>(null); final error = ValueNotifier<String?>(null);
return await showAdaptiveDialog<int>( return await showAdaptiveDialog<int>(
context: context, context: context,
@ -22,7 +22,7 @@ Future<int?> showPermissionChooser(
crossAxisAlignment: .stretch, crossAxisAlignment: .stretch,
spacing: 12.0, spacing: 12.0,
children: [ children: [
Text(L10n.of(context).setPermissionsLevelDescription), Text(L10n.of(context).setPowerLevelDescription),
ValueListenableBuilder( ValueListenableBuilder(
valueListenable: error, valueListenable: error,
builder: (context, errorText, _) => DialogTextField( builder: (context, errorText, _) => DialogTextField(
@ -38,8 +38,6 @@ Future<int?> showPermissionChooser(
), ),
actions: [ actions: [
AdaptiveDialogAction( AdaptiveDialogAction(
bigButtons: true,
borderRadius: AdaptiveDialogAction.topRadius,
onPressed: () { onPressed: () {
final level = int.tryParse(controller.text.trim()); final level = int.tryParse(controller.text.trim());
if (level == null) { if (level == null) {
@ -52,31 +50,12 @@ Future<int?> showPermissionChooser(
} }
Navigator.of(context).pop<int>(level); Navigator.of(context).pop<int>(level);
}, },
child: Text(L10n.of(context).setCustomPermissionLevel), child: Text(L10n.of(context).setPowerLevel),
),
AdaptiveDialogAction(
onPressed: () => Navigator.of(context).pop<int>(null),
child: Text(L10n.of(context).cancel),
), ),
if (maxLevel >= 100 && currentLevel != 100)
AdaptiveDialogAction(
borderRadius: AdaptiveDialogAction.centerRadius,
bigButtons: true,
onPressed: () => Navigator.of(context).pop<int>(100),
child: Text(L10n.of(context).admin),
),
if (maxLevel >= 50 && currentLevel != 50)
AdaptiveDialogAction(
borderRadius: maxLevel != 0
? AdaptiveDialogAction.centerRadius
: AdaptiveDialogAction.bottomRadius,
bigButtons: true,
onPressed: () => Navigator.of(context).pop<int>(50),
child: Text(L10n.of(context).moderator),
),
if (currentLevel != 0)
AdaptiveDialogAction(
borderRadius: AdaptiveDialogAction.bottomRadius,
bigButtons: true,
onPressed: () => Navigator.of(context).pop<int>(0),
child: Text(L10n.of(context).normalUser),
),
], ],
), ),
); );