refactor: Better UX for create space children

This commit is contained in:
Christian Kußowski 2026-02-28 15:52:57 +01:00
commit 9fd85a8b58
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
3 changed files with 76 additions and 116 deletions

View file

@ -176,8 +176,11 @@ abstract class AppRoutes {
), ),
GoRoute( GoRoute(
path: 'newgroup', path: 'newgroup',
pageBuilder: (context, state) => pageBuilder: (context, state) => defaultPageBuilder(
defaultPageBuilder(context, state, const NewGroup()), context,
state,
NewGroup(spaceId: state.uri.queryParameters['space_id']),
),
redirect: loggedOutRedirect, redirect: loggedOutRedirect,
), ),
GoRoute( GoRoute(
@ -185,7 +188,10 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) => defaultPageBuilder(
context, context,
state, state,
const NewGroup(createGroupType: CreateGroupType.space), NewGroup(
createGroupType: CreateGroupType.space,
spaceId: state.uri.queryParameters['space_id'],
),
), ),
redirect: loggedOutRedirect, redirect: loggedOutRedirect,
), ),

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -15,14 +16,11 @@ import 'package:fluffychat/utils/localized_exception_extension.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_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/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/hover_builder.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
enum AddRoomType { chat, subspace }
enum SpaceChildAction { enum SpaceChildAction {
mute, mute,
unmute, unmute,
@ -59,13 +57,30 @@ class _SpaceViewState extends State<SpaceView> {
bool _noMoreRooms = false; bool _noMoreRooms = false;
bool _isLoading = false; bool _isLoading = false;
StreamSubscription? _childStateSub;
@override @override
void initState() { void initState() {
_loadHierarchy(); _loadHierarchy();
_childStateSub = Matrix.of(context).client.onSync.stream
.where(
(syncUpdate) =>
syncUpdate.rooms?.join?[widget.spaceId]?.timeline?.events?.any(
(event) => event.type == EventTypes.SpaceChild,
) ??
false,
)
.listen(_loadHierarchy);
super.initState(); super.initState();
} }
Future<void> _loadHierarchy() async { @override
void dispose() {
_childStateSub?.cancel();
super.dispose();
}
Future<void> _loadHierarchy([_]) async {
final matrix = Matrix.of(context); final matrix = Matrix.of(context);
final room = matrix.client.getRoomById(widget.spaceId); final room = matrix.client.getRoomById(widget.spaceId);
if (room == null) return; if (room == null) return;
@ -189,83 +204,6 @@ class _SpaceViewState extends State<SpaceView> {
} }
} }
Future<void> _addChatOrSubspace(AddRoomType roomType) async {
final names = await showTextInputDialog(
context: context,
title: roomType == AddRoomType.subspace
? L10n.of(context).newSubSpace
: L10n.of(context).createGroup,
hintText: roomType == AddRoomType.subspace
? L10n.of(context).spaceName
: L10n.of(context).groupName,
minLines: 1,
maxLines: 1,
maxLength: 64,
validator: (text) {
if (text.isEmpty) {
return L10n.of(context).pleaseChoose;
}
return null;
},
okLabel: L10n.of(context).create,
cancelLabel: L10n.of(context).cancel,
);
if (names == null) return;
final client = Matrix.of(context).client;
final result = await showFutureLoadingDialog(
context: context,
future: () async {
late final String roomId;
final activeSpace = client.getRoomById(widget.spaceId)!;
await activeSpace.postLoad();
final isPublicSpace = activeSpace.joinRules == JoinRules.public;
if (roomType == AddRoomType.subspace) {
roomId = await client.createSpace(
name: names,
visibility: isPublicSpace
? sdk.Visibility.public
: sdk.Visibility.private,
);
} else {
roomId = await client.createGroupChat(
enableEncryption: !isPublicSpace,
groupName: names,
preset: isPublicSpace
? CreateRoomPreset.publicChat
: CreateRoomPreset.privateChat,
visibility: isPublicSpace
? sdk.Visibility.public
: sdk.Visibility.private,
initialState: isPublicSpace
? null
: [
StateEvent(
content: {
'join_rule': 'restricted',
'allow': [
{
'room_id': widget.spaceId,
'type': 'm.room_membership',
},
],
},
type: EventTypes.RoomJoinRules,
),
],
);
}
await activeSpace.setSpaceChild(roomId);
},
);
if (result.error != null) return;
setState(() {
_nextBatch = null;
_discoveredChildren.clear();
});
_loadHierarchy();
}
Future<void> _showSpaceChildEditMenu( Future<void> _showSpaceChildEditMenu(
BuildContext posContext, BuildContext posContext,
String roomId, String roomId,
@ -363,9 +301,17 @@ class _SpaceViewState extends State<SpaceView> {
child: Row( child: Row(
mainAxisSize: .min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.remove), Icon(
Icons.remove,
color: Theme.of(context).colorScheme.onErrorContainer,
),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(L10n.of(context).removeFromSpace), Text(
L10n.of(context).removeFromSpace,
style: TextStyle(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
], ],
), ),
), ),
@ -389,7 +335,6 @@ class _SpaceViewState extends State<SpaceView> {
if (result.isError) return; if (result.isError) return;
if (!mounted) return; if (!mounted) return;
_nextBatch = null; _nextBatch = null;
_loadHierarchy();
return; return;
case SpaceChildAction.mute: case SpaceChildAction.mute:
await showFutureLoadingDialog( await showFutureLoadingDialog(
@ -455,34 +400,11 @@ class _SpaceViewState extends State<SpaceView> {
), ),
actions: [ actions: [
if (isAdmin) if (isAdmin)
PopupMenuButton<AddRoomType>( IconButton(
icon: const Icon(Icons.add_outlined), icon: Icon(Icons.add_outlined),
onSelected: _addChatOrSubspace,
tooltip: L10n.of(context).addChatOrSubSpace, tooltip: L10n.of(context).addChatOrSubSpace,
itemBuilder: (context) => [ onPressed: () =>
PopupMenuItem( context.go('/rooms/newgroup?space_id=${widget.spaceId}'),
value: AddRoomType.chat,
child: Row(
mainAxisSize: .min,
children: [
const Icon(Icons.group_add_outlined),
const SizedBox(width: 12),
Text(L10n.of(context).newGroup),
],
),
),
PopupMenuItem(
value: AddRoomType.subspace,
child: Row(
mainAxisSize: .min,
children: [
const Icon(Icons.workspaces_outlined),
const SizedBox(width: 12),
Text(L10n.of(context).newSubSpace),
],
),
),
],
), ),
PopupMenuButton<SpaceActions>( PopupMenuButton<SpaceActions>(
useRootNavigator: true, useRootNavigator: true,

View file

@ -14,7 +14,12 @@ import 'package:fluffychat/widgets/matrix.dart';
class NewGroup extends StatefulWidget { class NewGroup extends StatefulWidget {
final CreateGroupType createGroupType; final CreateGroupType createGroupType;
const NewGroup({this.createGroupType = CreateGroupType.group, super.key}); final String? spaceId;
const NewGroup({
this.createGroupType = CreateGroupType.group,
this.spaceId,
super.key,
});
@override @override
NewGroupController createState() => NewGroupController(); NewGroupController createState() => NewGroupController();
@ -63,7 +68,9 @@ class NewGroupController extends State<NewGroup> {
Future<void> _createGroup() async { Future<void> _createGroup() async {
if (!mounted) return; if (!mounted) return;
final roomId = await Matrix.of(context).client.createGroupChat( final client = Matrix.of(context).client;
final roomId = await client.createGroupChat(
visibility: groupCanBeFound visibility: groupCanBeFound
? sdk.Visibility.public ? sdk.Visibility.public
: sdk.Visibility.private, : sdk.Visibility.private,
@ -79,7 +86,9 @@ class NewGroupController extends State<NewGroup> {
), ),
], ],
); );
await _addToSpace(roomId);
if (!mounted) return; if (!mounted) return;
context.go('/rooms/$roomId/invite'); context.go('/rooms/$roomId/invite');
} }
@ -104,10 +113,23 @@ class NewGroupController extends State<NewGroup> {
), ),
], ],
); );
await _addToSpace(spaceId);
if (!mounted) return; if (!mounted) return;
context.pop<String>(spaceId); context.pop<String>(spaceId);
} }
Future<void> _addToSpace(String roomId) async {
final spaceId = widget.spaceId;
if (spaceId != null) {
final activeSpace = Matrix.of(context).client.getRoomById(spaceId);
if (activeSpace == null) {
throw Exception('Can not add group to space: Space not found $spaceId');
}
await activeSpace.postLoad();
await activeSpace.setSpaceChild(roomId);
}
}
Future<void> submitAction([_]) async { Future<void> submitAction([_]) async {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
@ -143,6 +165,16 @@ class NewGroupController extends State<NewGroup> {
} }
} }
@override
void initState() {
final spaceId = widget.spaceId;
if (spaceId != null) {
final space = Matrix.of(context).client.getRoomById(spaceId);
publicGroup = space?.joinRules == JoinRules.public;
}
super.initState();
}
@override @override
Widget build(BuildContext context) => NewGroupView(this); Widget build(BuildContext context) => NewGroupView(this);
} }