chore: Better context menu for space children

This commit is contained in:
Christian Kußowski 2026-02-28 15:34:36 +01:00
commit 0061c948cd
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652

View file

@ -12,10 +12,8 @@ import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat_list/unread_bubble.dart'; import 'package:fluffychat/pages/chat_list/unread_bubble.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart';
import 'package:fluffychat/utils/string_color.dart'; import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_modal_action_popup.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
@ -25,7 +23,14 @@ import 'package:fluffychat/widgets/matrix.dart';
enum AddRoomType { chat, subspace } enum AddRoomType { chat, subspace }
enum SpaceChildAction { edit, moveToSpace, removeFromSpace } enum SpaceChildAction {
mute,
unmute,
markAsUnread,
markAsRead,
removeFromSpace,
leave,
}
enum SpaceActions { settings, invite, members, leave } enum SpaceActions { settings, invite, members, leave }
@ -265,6 +270,10 @@ class _SpaceViewState extends State<SpaceView> {
BuildContext posContext, BuildContext posContext,
String roomId, String roomId,
) async { ) async {
final client = Matrix.of(context).client;
final space = client.getRoomById(widget.spaceId);
final room = client.getRoomById(roomId);
if (space == null) return;
final overlay = final overlay =
Overlay.of(posContext).context.findRenderObject() as RenderBox; Overlay.of(posContext).context.findRenderObject() as RenderBox;
@ -285,85 +294,86 @@ class _SpaceViewState extends State<SpaceView> {
context: posContext, context: posContext,
position: position, position: position,
items: [ items: [
PopupMenuItem( if (room != null && room.membership == Membership.join) ...[
value: SpaceChildAction.moveToSpace, PopupMenuItem(
child: Row( value: room.pushRuleState == PushRuleState.notify
mainAxisSize: .min, ? SpaceChildAction.mute
children: [ : SpaceChildAction.unmute,
const Icon(Icons.move_down_outlined), child: Row(
const SizedBox(width: 12), mainAxisSize: .min,
Text(L10n.of(context).moveToDifferentSpace), children: [
], Icon(
room.pushRuleState == PushRuleState.notify
? Icons.notifications_off_outlined
: Icons.notifications_on_outlined,
),
const SizedBox(width: 12),
Text(
room.pushRuleState == PushRuleState.notify
? L10n.of(context).muteChat
: L10n.of(context).unmuteChat,
),
],
),
), ),
), PopupMenuItem(
PopupMenuItem( value: room.markedUnread
value: SpaceChildAction.edit, ? SpaceChildAction.markAsRead
child: Row( : SpaceChildAction.markAsUnread,
mainAxisSize: .min, child: Row(
children: [ mainAxisSize: .min,
const Icon(Icons.edit_outlined), children: [
const SizedBox(width: 12), Icon(
Text(L10n.of(context).edit), room.markedUnread
], ? Icons.mark_as_unread
: Icons.mark_as_unread_outlined,
),
const SizedBox(width: 12),
Text(
room.isUnread
? L10n.of(context).markAsRead
: L10n.of(context).markAsUnread,
),
],
),
), ),
), PopupMenuItem(
PopupMenuItem( value: SpaceChildAction.leave,
value: SpaceChildAction.removeFromSpace, child: Row(
child: Row( mainAxisSize: .min,
mainAxisSize: .min, children: [
children: [ Icon(
const Icon(Icons.group_remove_outlined), Icons.delete_outlined,
const SizedBox(width: 12), color: Theme.of(context).colorScheme.onErrorContainer,
Text(L10n.of(context).removeFromSpace), ),
], const SizedBox(width: 12),
Text(
L10n.of(context).leave,
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
],
),
),
],
if (space.canChangeStateEvent(EventTypes.SpaceChild) == true)
PopupMenuItem(
value: SpaceChildAction.removeFromSpace,
child: Row(
mainAxisSize: .min,
children: [
const Icon(Icons.remove),
const SizedBox(width: 12),
Text(L10n.of(context).removeFromSpace),
],
),
), ),
),
], ],
); );
if (action == null) return; if (action == null) return;
if (!mounted) return; if (!mounted) return;
final space = Matrix.of(context).client.getRoomById(widget.spaceId);
if (space == null) return;
switch (action) { switch (action) {
case SpaceChildAction.edit:
context.push('/rooms/$roomId/details');
case SpaceChildAction.moveToSpace:
final spacesWithPowerLevels = space.client.rooms
.where(
(room) =>
room.isSpace &&
room.canChangeStateEvent(EventTypes.SpaceChild) &&
room.id != widget.spaceId,
)
.toList();
final newSpace = await showModalActionPopup(
context: context,
title: L10n.of(context).space,
actions: spacesWithPowerLevels
.map(
(space) => AdaptiveModalAction(
value: space,
label: space.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)),
),
),
)
.toList(),
);
if (newSpace == null) return;
final result = await showFutureLoadingDialog(
context: context,
future: () async {
await newSpace.setSpaceChild(newSpace.id);
await space.removeSpaceChild(roomId);
},
);
if (result.isError) return;
if (!mounted) return;
_nextBatch = null;
_loadHierarchy();
return;
case SpaceChildAction.removeFromSpace: case SpaceChildAction.removeFromSpace:
final consent = await showOkCancelAlertDialog( final consent = await showOkCancelAlertDialog(
context: context, context: context,
@ -381,6 +391,31 @@ class _SpaceViewState extends State<SpaceView> {
_nextBatch = null; _nextBatch = null;
_loadHierarchy(); _loadHierarchy();
return; return;
case SpaceChildAction.mute:
await showFutureLoadingDialog(
context: context,
future: () => room!.setPushRuleState(PushRuleState.mentionsOnly),
);
case SpaceChildAction.unmute:
await showFutureLoadingDialog(
context: context,
future: () => room!.setPushRuleState(PushRuleState.notify),
);
case SpaceChildAction.markAsUnread:
await showFutureLoadingDialog(
context: context,
future: () => room!.markUnread(true),
);
case SpaceChildAction.markAsRead:
await showFutureLoadingDialog(
context: context,
future: () => room!.markUnread(false),
);
case SpaceChildAction.leave:
await showFutureLoadingDialog(
context: context,
future: () => room!.leave(),
);
} }
} }
@ -580,6 +615,7 @@ class _SpaceViewState extends State<SpaceView> {
if (joinedRoom?.membership == Membership.leave) { if (joinedRoom?.membership == Membership.leave) {
joinedRoom = null; joinedRoom = null;
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
@ -607,13 +643,13 @@ class _SpaceViewState extends State<SpaceView> {
onTap: joinedRoom != null onTap: joinedRoom != null
? () => widget.onChatTab(joinedRoom!) ? () => widget.onChatTab(joinedRoom!)
: null, : null,
onLongPress: isAdmin onLongPress: joinedRoom != null
? () => _showSpaceChildEditMenu( ? () => _showSpaceChildEditMenu(
context, context,
item.roomId, item.roomId,
) )
: null, : null,
leading: hovered && isAdmin leading: hovered
? SizedBox.square( ? SizedBox.square(
dimension: avatarSize, dimension: avatarSize,
child: IconButton( child: IconButton(
@ -627,11 +663,13 @@ class _SpaceViewState extends State<SpaceView> {
.colorScheme .colorScheme
.tertiaryContainer, .tertiaryContainer,
), ),
onPressed: () => onPressed:
_showSpaceChildEditMenu( isAdmin || joinedRoom != null
context, ? () => _showSpaceChildEditMenu(
item.roomId, context,
), item.roomId,
)
: null,
icon: const Icon(Icons.edit_outlined), icon: const Icon(Icons.edit_outlined),
), ),
) )
@ -676,6 +714,16 @@ class _SpaceViewState extends State<SpaceView> {
), ),
), ),
), ),
if (joinedRoom != null &&
joinedRoom.pushRuleState !=
PushRuleState.notify)
const Padding(
padding: EdgeInsets.only(left: 4.0),
child: Icon(
Icons.notifications_off_outlined,
size: 16,
),
),
if (joinedRoom != null) if (joinedRoom != null)
UnreadBubble(room: joinedRoom) UnreadBubble(room: joinedRoom)
else else