nexus/lib/controllers/spaces.dart
2026-10-23 20:26:37 -04:00

171 lines
5.2 KiB
Dart

import "package:collection/collection.dart";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:material_ui/material_ui.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/account_data.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/controllers/settings.dart";
import "package:nexus/controllers/top_level_spaces.dart";
import "package:nexus/controllers/space_edges.dart";
import "package:nexus/models/room.dart";
import "package:nexus/models/room_metadata.dart";
import "package:nexus/models/space.dart";
import "package:nexus/models/subspace.dart";
class SpacesController extends AsyncNotifier<IList<Space>> {
@override
Future<IList<Space>> build() async {
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 childrenById = spaceEdges.map(
(spaceId, edges) =>
MapEntry(spaceId, edges.map((edge) => edge.childId).toList()),
);
/// Everything under [spaceId], however deep, skipping any already
/// [visited] so cycles end.
ISet<String> collectDescendants(
String spaceId, [
ISet<String> visited = const .empty(),
]) => (childrenById[spaceId] ?? const []).fold(
visited,
(visited, childId) => visited.contains(childId)
? visited
: collectDescendants(childId, visited.add(childId)),
);
Space buildSpace(String spaceId) {
final space = rooms[spaceId];
final directChildrenIds = childrenById[spaceId] ?? const [];
return .new(
id: spaceId,
room: space,
title: space?.metadata?.name ?? "Unnamed Space",
children: .new(
directChildrenIds
.where((childId) => !childrenById.containsKey(childId))
.map((childId) => rooms[childId])
.nonNulls,
),
subSpaces: .new(
directChildrenIds
.where(childrenById.containsKey)
.map(
(childId) => switch (rooms[childId]) {
final room? => Subspace(
room: room,
children: .new(
collectDescendants(childId)
.map((id) => rooms[id])
.nonNulls,
),
),
null => null,
},
)
.nonNulls,
),
);
}
final spaces = topLevelSpaceIds
.where(rooms.containsKey)
.map(buildSpace)
.toIList();
final usedRooms = spaces
.expand(
(space) => [
...space.children,
...space.subSpaces.expand((s) => s.children),
],
)
.toISet();
final usedRoomIds = usedRooms.map((room) => room.metadata?.id).toSet();
final directMessages = accountData.directMessages.values.flattened.toSet();
final otherRooms = rooms.entries
.where(
(e) =>
!usedRoomIds.contains(e.key) &&
!topLevelSpaceIds.contains(e.key) &&
!childrenById.containsKey(e.key),
)
.map((e) => e.value)
.toIList();
final homeRooms = otherRooms
.addAll(
await ref.watch(
SettingsController.provider.selectAsync(
(data) => data.showAllInHome,
),
)
? usedRooms
: .empty(),
)
.where((r) => !directMessages.contains(r.metadata?.id))
.toIList();
final dmRooms = otherRooms
.where((r) => directMessages.contains(r.metadata?.id))
.toIList();
final allSpaces = <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(),
),
...spaces,
];
final roomOrder = await ref.watch(
SettingsController.provider.selectAsync((data) => data.roomOrder),
);
// Mentioned -> Unread -> Other
int unreadPriority(Room room) => switch (room.metadata) {
RoomMetadata(unreadHighlights: > 0) => 0,
RoomMetadata(unreadNotifications: > 0) ||
RoomMetadata(unreadMessages: > 0) => 1,
_ => 2,
};
return allSpaces
.map(
(space) => space.copyWith(
children: space.children.sort((a, b) {
if (roomOrder == .unreadFirst) {
final byUnread = unreadPriority(a).compareTo(unreadPriority(b));
if (byUnread != 0) return byUnread;
}
// Most recent activity first
return (b.metadata?.sortingTimestamp ?? DateTime(0)).compareTo(
a.metadata?.sortingTimestamp ?? DateTime(0),
);
}),
),
)
.toIList();
}
static final provider = AsyncNotifierProvider<SpacesController, IList<Space>>(
SpacesController.new,
);
}