From e1d7a30a0649f5856c3fcd8ed2567c7451bb0c39 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 15:11:40 -0400 Subject: [PATCH] Support one level of subspaces --- lib/controllers/spaces_controller.dart | 172 +++++++++++++------------ lib/widgets/sidebar.dart | 3 +- 2 files changed, 93 insertions(+), 82 deletions(-) diff --git a/lib/controllers/spaces_controller.dart b/lib/controllers/spaces_controller.dart index 26e1487..03a6b8a 100644 --- a/lib/controllers/spaces_controller.dart +++ b/lib/controllers/spaces_controller.dart @@ -6,7 +6,9 @@ import "package:nexus/controllers/account_data_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/top_level_spaces_controller.dart"; import "package:nexus/controllers/space_edges_controller.dart"; +import "package:nexus/models/room.dart"; import "package:nexus/models/space.dart"; +import "package:nexus/models/subspace.dart"; class SpacesController extends Notifier> { @override @@ -14,107 +16,115 @@ class SpacesController extends Notifier> { final rooms = ref.watch(RoomsController.provider); final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider); final spaceEdges = ref.watch(SpaceEdgesController.provider); + final accountData = ref.watch(AccountDataController.provider); - final childRoomsBySpaceId = IMap.fromEntries( - topLevelSpaceIds.map((spaceId) { - ISet walk(String currentId) { - final children = spaceEdges[currentId] ?? .new(); + final childrenById = { + for (final entry in spaceEdges.entries) + entry.key: entry.value.map((e) => e.childId).toList(), + }; - return children.fold>(.new(), (acc, edge) { - final childId = edge.childId; - final isSpace = spaceEdges.containsKey(childId); + Set collectDescendants(String startId) { + final visited = {}; + final stack = [startId]; - return acc - .addAll(!isSpace ? ISet([childId]) : const .empty()) - .addAll(isSpace ? walk(childId) : const .empty()); - }); + while (stack.isNotEmpty) { + final current = stack.removeLast(); + final children = childrenById[current] ?? const []; + + for (final child in children) { + if (visited.add(child)) { + stack.add(child); + } } + } - return MapEntry( - spaceId, - walk(spaceId).map((id) => rooms[id]).nonNulls.toIList(), - ); - }), - ); + return visited; + } - final allNestedRoomIds = childRoomsBySpaceId.values - .expand((l) => l) - .map( - (room) => rooms.entries - .firstWhere( - (entry) => entry.value.metadata?.id == room.metadata?.id, - ) - .key, - ) - .toISet(); + Space buildSpace(String spaceId) { + final space = rooms[spaceId]; + final directChildrenIds = childrenById[spaceId] ?? const []; + + final directRooms = []; + final subSpaces = []; + + for (final childId in directChildrenIds) { + final room = rooms[childId]; + if (room == null) continue; + + if (childrenById.containsKey(childId)) { + final descendants = collectDescendants(childId); + + subSpaces.add( + .new( + room: room, + children: .new(descendants.map((id) => rooms[id]).nonNulls), + ), + ); + } else { + directRooms.add(room); + } + } + + return .new( + id: spaceId, + room: space, + title: space?.metadata?.name ?? "Unnamed Space", + children: .new(directRooms), + subSpaces: .new(subSpaces), + ); + } + + final spaces = topLevelSpaceIds.map(buildSpace).toIList(); + + final usedRoomIds = { + for (final space in spaces) ...[ + ...space.children.map((r) => r.metadata?.id), + ...space.subSpaces.expand((s) => s.children.map((r) => r.metadata?.id)), + ], + }.nonNulls.toISet(); + + final directMessages = IMap( + accountData["m.direct"]?.content ?? {}, + ).values.expand((e) => e).toISet(); final otherRooms = rooms.entries .where( (e) => - !allNestedRoomIds.contains(e.key) && + !usedRoomIds.contains(e.key) && !topLevelSpaceIds.contains(e.key) && - !spaceEdges.containsKey(e.key), + !childrenById.containsKey(e.key), ) - .map((e) => e.value); - - final accountData = ref.watch(AccountDataController.provider); - - final directMessages = IMap( - accountData["m.direct"]?.content ?? {}, - ).values.expand((element) => element); + .map((e) => e.value) + .toIList(); final homeRooms = otherRooms - .where( - (room) => - directMessages.any( - (directMessage) => directMessage == room.metadata?.id, - ) == - false, - ) + .where((r) => !directMessages.contains(r.metadata?.id)) .toIList(); final dmRooms = otherRooms - .where( - (room) => directMessages.any( - (directMessage) => directMessage == room.metadata?.id, - ), - ) + .where((r) => directMessages.contains(r.metadata?.id)) .toIList(); - final topLevelSpacesList = topLevelSpaceIds - .map((id) { - final room = rooms[id]; - if (room == null) return null; + final allSpaces = [ + .new( + id: "home", + title: "Home", + icon: Icons.home, + children: homeRooms, + subSpaces: .new(), + ), + .new( + id: "dms", + title: "Direct Messages", + icon: Icons.people, + children: dmRooms, + subSpaces: .new(), + ), + ...spaces, + ]; - final children = childRoomsBySpaceId[id] ?? .new(); - return Space( - subSpaces: const IList.empty(), // TODO - id: id, - title: room.metadata?.name ?? "Unnamed Room", - room: room, - children: children, - ); - }) - .nonNulls - .toIList(); - - return [ - .new( - id: "home", - title: "Home", - icon: Icons.home, - children: homeRooms, - subSpaces: .new(), - ), - .new( - id: "dms", - title: "Direct Messages", - icon: Icons.people, - children: dmRooms, - subSpaces: .new(), - ), - ...topLevelSpacesList, - ] + return allSpaces .map( (space) => space.copyWith( children: .new( diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index 8a6ed23..abe9a0a 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -213,12 +213,13 @@ class Sidebar extends HookConsumerWidget { selectedIndex: selectedRoomIndex ?? 0, sections: [ .new( + header: selectedSpace.room == null ? null : Text("Rooms"), destinations: roomsToDestinations(selectedSpace.children), ), for (final subSpace in selectedSpace.subSpaces) .new( header: Text( - subSpace.room.metadata?.name ?? "Unnamed Room", + subSpace.room.metadata?.name ?? "Unnamed Space", ), destinations: roomsToDestinations(subSpace.children), ),