Support one level of subspaces

This commit is contained in:
Henry Hiles 2026-06-06 15:11:40 -04:00
commit e1d7a30a06
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
2 changed files with 93 additions and 82 deletions

View file

@ -6,7 +6,9 @@ import "package:nexus/controllers/account_data_controller.dart";
import "package:nexus/controllers/rooms_controller.dart"; import "package:nexus/controllers/rooms_controller.dart";
import "package:nexus/controllers/top_level_spaces_controller.dart"; import "package:nexus/controllers/top_level_spaces_controller.dart";
import "package:nexus/controllers/space_edges_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/space.dart";
import "package:nexus/models/subspace.dart";
class SpacesController extends Notifier<IList<Space>> { class SpacesController extends Notifier<IList<Space>> {
@override @override
@ -14,107 +16,115 @@ class SpacesController extends Notifier<IList<Space>> {
final rooms = ref.watch(RoomsController.provider); final rooms = ref.watch(RoomsController.provider);
final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider); final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider);
final spaceEdges = ref.watch(SpaceEdgesController.provider); final spaceEdges = ref.watch(SpaceEdgesController.provider);
final accountData = ref.watch(AccountDataController.provider);
final childRoomsBySpaceId = IMap.fromEntries( final childrenById = {
topLevelSpaceIds.map((spaceId) { for (final entry in spaceEdges.entries)
ISet<String> walk(String currentId) { entry.key: entry.value.map((e) => e.childId).toList(),
final children = spaceEdges[currentId] ?? .new(); };
return children.fold<ISet<String>>(.new(), (acc, edge) { Set<String> collectDescendants(String startId) {
final childId = edge.childId; final visited = <String>{};
final isSpace = spaceEdges.containsKey(childId); final stack = [startId];
return acc while (stack.isNotEmpty) {
.addAll(!isSpace ? ISet([childId]) : const .empty()) final current = stack.removeLast();
.addAll(isSpace ? walk(childId) : const .empty()); final children = childrenById[current] ?? const [];
});
for (final child in children) {
if (visited.add(child)) {
stack.add(child);
}
} }
}
return MapEntry( return visited;
spaceId, }
walk(spaceId).map((id) => rooms[id]).nonNulls.toIList(),
);
}),
);
final allNestedRoomIds = childRoomsBySpaceId.values Space buildSpace(String spaceId) {
.expand((l) => l) final space = rooms[spaceId];
.map( final directChildrenIds = childrenById[spaceId] ?? const [];
(room) => rooms.entries
.firstWhere( final directRooms = <Room>[];
(entry) => entry.value.metadata?.id == room.metadata?.id, final subSpaces = <Subspace>[];
)
.key, for (final childId in directChildrenIds) {
) final room = rooms[childId];
.toISet(); 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 final otherRooms = rooms.entries
.where( .where(
(e) => (e) =>
!allNestedRoomIds.contains(e.key) && !usedRoomIds.contains(e.key) &&
!topLevelSpaceIds.contains(e.key) && !topLevelSpaceIds.contains(e.key) &&
!spaceEdges.containsKey(e.key), !childrenById.containsKey(e.key),
) )
.map((e) => e.value); .map((e) => e.value)
.toIList();
final accountData = ref.watch(AccountDataController.provider);
final directMessages = IMap(
accountData["m.direct"]?.content ?? {},
).values.expand((element) => element);
final homeRooms = otherRooms final homeRooms = otherRooms
.where( .where((r) => !directMessages.contains(r.metadata?.id))
(room) =>
directMessages.any(
(directMessage) => directMessage == room.metadata?.id,
) ==
false,
)
.toIList(); .toIList();
final dmRooms = otherRooms final dmRooms = otherRooms
.where( .where((r) => directMessages.contains(r.metadata?.id))
(room) => directMessages.any(
(directMessage) => directMessage == room.metadata?.id,
),
)
.toIList(); .toIList();
final topLevelSpacesList = topLevelSpaceIds final allSpaces = <Space>[
.map((id) { .new(
final room = rooms[id]; id: "home",
if (room == null) return null; 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 allSpaces
return Space(
subSpaces: const IList.empty(), // TODO
id: id,
title: room.metadata?.name ?? "Unnamed Room",
room: room,
children: children,
);
})
.nonNulls
.toIList();
return <Space>[
.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,
]
.map( .map(
(space) => space.copyWith( (space) => space.copyWith(
children: .new( children: .new(

View file

@ -213,12 +213,13 @@ class Sidebar extends HookConsumerWidget {
selectedIndex: selectedRoomIndex ?? 0, selectedIndex: selectedRoomIndex ?? 0,
sections: [ sections: [
.new( .new(
header: selectedSpace.room == null ? null : Text("Rooms"),
destinations: roomsToDestinations(selectedSpace.children), destinations: roomsToDestinations(selectedSpace.children),
), ),
for (final subSpace in selectedSpace.subSpaces) for (final subSpace in selectedSpace.subSpaces)
.new( .new(
header: Text( header: Text(
subSpace.room.metadata?.name ?? "Unnamed Room", subSpace.room.metadata?.name ?? "Unnamed Space",
), ),
destinations: roomsToDestinations(subSpace.children), destinations: roomsToDestinations(subSpace.children),
), ),