From 22aa26fd052625cccf4fe36d5f3ed395aea81726 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 10:15:29 -0400 Subject: [PATCH 01/11] fix keyboard navigation of forms Including submit with keyboard, and not defocusing on submit of a form --- lib/pages/login_page.dart | 3 +++ lib/pages/verify_page.dart | 37 ++++++++++++++++++++----------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 4969287..5c9d53d 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -80,6 +80,9 @@ class LoginPage extends HookConsumerWidget { validator: requiredValidator, controller: password, obscureText: true, + onFieldSubmitted: (_) => tryLogin(), + // Don't defocus on submit + onEditingComplete: () {}, ), ], ), diff --git a/lib/pages/verify_page.dart b/lib/pages/verify_page.dart index a2089e3..bf5c9f3 100644 --- a/lib/pages/verify_page.dart +++ b/lib/pages/verify_page.dart @@ -15,6 +15,22 @@ class VerifyPage extends HookConsumerWidget { final inputError = useState(null); final formKey = useRef(GlobalKey()); + Future verify() async { + isLoading.value = true; + + try { + if (formKey.value.currentState?.validate() != true) { + return; + } + + inputError.value = await ref + .watch(ClientController.provider.notifier) + .verify(passphraseController.text); + } finally { + isLoading.value = false; + } + } + return Scaffold( appBar: Appbar(), body: AlertDialog( @@ -40,29 +56,16 @@ class VerifyPage extends HookConsumerWidget { label: Text("Recovery Key or Passphrase"), errorText: inputError.value, ), + onFieldSubmitted: (_) => verify(), + // Don't defocus on submit + onEditingComplete: () {}, ), ], ), ), actions: [ TextButton( - onPressed: isLoading.value - ? null - : () async { - isLoading.value = true; - - try { - if (formKey.value.currentState?.validate() != true) { - return; - } - - inputError.value = await ref - .watch(ClientController.provider.notifier) - .verify(passphraseController.text); - } finally { - isLoading.value = false; - } - }, + onPressed: isLoading.value ? null : verify, child: Text("Verify"), ), ], From c9aa0173d873b8510cee8285ba82bd7daf01fce0 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 10:18:40 -0400 Subject: [PATCH 02/11] fix navigation rail hash --- linux/nix/pkg/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/nix/pkg/default.nix b/linux/nix/pkg/default.nix index 7e7b427..c879f24 100644 --- a/linux/nix/pkg/default.nix +++ b/linux/nix/pkg/default.nix @@ -32,7 +32,7 @@ flutter.buildFlutterApplication { window_size = "sha256-XelNtp7tpZ91QCEcvewVphNUtgQX7xrp5QP0oFo6DgM="; emoji_text_field = "sha256-3TOys09EP2GRo6pUBGPXaqBlE39O2Cmwt42Hs1cTDKo="; linkify = "sha256-mxV/XHLxF9cn7sUPr2SUNjVmDr5lbxkuGCbNdyiZi2c="; - navigation_rail_m3e = "sha256-Zkqo8RkNSWiELB+Bk6iLsgft9THuJiMtysEwXNS/ZgY="; + navigation_rail_m3e = "sha256-+2awDTQnK58gGRY1nuHckG/jjxarsYSRu9ovR4i4TEc="; }; postInstall = '' From 457de3c77c05d245b6caee39edf206b8340cb455 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 10:51:56 -0400 Subject: [PATCH 03/11] more performant member list --- .../members_grouped_controller.dart | 32 ++-- lib/widgets/member_list.dart | 137 +++++++++--------- pubspec.lock | 16 +- pubspec.yaml | 2 +- 4 files changed, 99 insertions(+), 88 deletions(-) diff --git a/lib/controllers/members_grouped_controller.dart b/lib/controllers/members_grouped_controller.dart index 7cec625..e07bcf3 100644 --- a/lib/controllers/members_grouped_controller.dart +++ b/lib/controllers/members_grouped_controller.dart @@ -8,12 +8,13 @@ import "package:nexus/models/content/create.dart"; import "package:nexus/models/content/power_levels.dart"; import "package:nexus/models/event.dart"; -class MembersGroupedController extends AsyncNotifier>> { +class MembersGroupedController + extends AsyncNotifier>>> { final MembersByStatusConfig config; MembersGroupedController(this.config); @override - Future>> build() async { + Future>>> build() async { final room = ref.watch( RoomsController.provider.select((value) => value[config.roomId]), ); @@ -42,23 +43,28 @@ class MembersGroupedController extends AsyncNotifier>> { MembersByStatusController.provider(config).future, ); - return members.fold>>(.new(), (result, event) { - final groupKey = creators?.contains(event.stateKey!) == true - ? null - : content.users[event.stateKey!] ?? content.usersDefault; + return members + .fold>>(.new(), (result, event) { + final groupKey = creators?.contains(event.stateKey!) == true + ? null + : content.users[event.stateKey!] ?? content.usersDefault; - return result.update( - groupKey, - (value) => value.add(event), - ifAbsent: () => .new({event}), - ); - }); + return result.update( + groupKey, + (value) => value.add(event), + ifAbsent: () => .new({event}), + ); + }) + .toEntryIList( + compare: (a, b) => + (b?.key ?? double.infinity).compareTo(a?.key ?? double.infinity), + ); } static final provider = AsyncNotifierProvider.family< MembersGroupedController, - IMap>, + IList>>, MembersByStatusConfig >(MembersGroupedController.new); } diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index bdac414..06c0b3c 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -3,7 +3,8 @@ import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:m3e_buttons/m3e_buttons.dart"; -import "package:material_segmented_list/material_segmented_list.dart"; +import "package:m3e_card_list/m3e_card_list.dart"; +import "package:nexus/controllers/members_by_status_controller.dart"; import "package:nexus/controllers/members_grouped_controller.dart"; import "package:nexus/helpers/extensions/get_localpart.dart"; import "package:nexus/helpers/extensions/string_to_color.dart"; @@ -20,19 +21,14 @@ class MemberList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final status = useState(MembershipStatus.join); - - final membersData = ref.watch( - MembersGroupedController.provider( - .new(roomId: roomId, status: status.value), - ), - ); + final statusIndex = useState(0); final options = { "Joined": .join, "Invited": .invite, "Banned": .ban, }; + final status = options.values.toIList()[statusIndex.value]; return Drawer( shape: Border(), @@ -54,16 +50,16 @@ class MemberList extends HookConsumerWidget { ), M3EToggleButtonGroup( type: .connected, - selectedIndex: options.values.toIList().indexOf(status.value), + selectedIndex: statusIndex.value, onSelectedIndexChanged: (index) => - status.value = options.values.elementAt(index ?? 0), + statusIndex.value = index ?? statusIndex.value, // overflow: M3EButtonGroupOverflow.menu, actions: options .mapTo( (name, value) => M3EToggleButtonGroupAction( checkedLabel: Text( - "$name${switch (membersData) { - AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.values.expand((element) => element).length})", + "$name${switch (ref.watch(MembersByStatusController.provider(.new(roomId: roomId, status: value)))) { + AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.length})", _ => "", }}", ), @@ -73,7 +69,11 @@ class MemberList extends HookConsumerWidget { .toList(), ), - switch (membersData) { + switch (ref.watch( + MembersGroupedController.provider( + .new(roomId: roomId, status: status), + ), + )) { AsyncError(:final error, :final stackTrace) => ErrorDialog( error, stackTrace, @@ -84,71 +84,76 @@ class MemberList extends HookConsumerWidget { child: Padding( padding: .symmetric(vertical: 18), child: Text( - "No ${options.keys.toIList()[options.values.toIList().indexOf(status.value)]} Members", + "No ${options.keys.toIList()[statusIndex.value]} Members", style: Theme.of(context).textTheme.headlineSmall, ), ), ) : Expanded( - child: ListView( - padding: .all(12), - children: [ + child: CustomScrollView( + slivers: [ for (final MapEntry(key: powerLevel, value: members) - in value.toEntryIList( - compare: (a, b) => (b?.key ?? double.infinity) - .compareTo(a?.key ?? double.infinity), - )) ...[ - Padding( - padding: .symmetric(horizontal: 4), - child: DividerText( - powerLevel == null - ? "Creators" - : "Power Level $powerLevel", + in value) ...[ + SliverToBoxAdapter( + child: Padding( + padding: .symmetric(horizontal: 16), + child: DividerText( + powerLevel == null + ? "Creators" + : "Power Level $powerLevel", + ), ), ), - SegmentedListSection( - children: members - .map( - (member) => switch (member.content) { - MembershipContent( - :final avatarUrl, - :final displayName, - ) => - SegmentedListTile( - onTap: () {}, - // context.showUserPopover( - // member.content as MembershipContent, - // member.stateKey!, - // roomId: roomId, - // globalPosition: details.globalPosition, - // ), - title: Text( - displayName ?? - member.stateKey!.localpart, - overflow: .ellipsis, - style: .new( - color: member.stateKey!.colorHash, - fontWeight: .bold, - ), - ), - subtitle: Text( - member.stateKey!, - overflow: .ellipsis, - ), - leading: AvatarOrHash( - avatarUrl, - displayName ?? - member.sender.localpart, + SliverM3ECardList( + padding: .all(4), + color: Theme.of( + context, + ).colorScheme.surfaceContainerHigh, + margin: .symmetric(horizontal: 12, vertical: 4), + itemCount: members.length, + itemBuilder: (context, index) => + switch (members[index].content) { + MembershipContent( + :final avatarUrl, + :final displayName, + ) => + ListTile( + title: Text( + displayName ?? + members[index] + .stateKey! + .localpart, + overflow: .ellipsis, + style: .new( + color: members[index] + .stateKey! + .colorHash, + fontWeight: .bold, ), ), - _ => throw Exception( - "Member content was not MembershipContent", + subtitle: Text( + members[index].stateKey!, + overflow: .ellipsis, + ), + leading: AvatarOrHash( + avatarUrl, + displayName ?? + members[index].sender.localpart, + ), ), - }, - ) - .toList(), + _ => throw Exception( + "Member content was not MembershipContent", + ), + }, + onTap: (index) { + // context.showUserPopover( + // member.content as MembershipContent, + // member.stateKey!, + // roomId: roomId, + // globalPosition: details.globalPosition, + // ), + }, ), - SizedBox(height: 4), ], ], ), diff --git a/pubspec.lock b/pubspec.lock index f44a05d..cc7ee27 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -783,6 +783,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + m3e_card_list: + dependency: "direct main" + description: + name: m3e_card_list + sha256: d4aba0123cccda40ac80789befa8d355e1dc16aa7dcee910157690b0546d78d6 + url: "https://pub.dev" + source: hosted + version: "0.1.0" m3e_design: dependency: transitive description: @@ -807,14 +815,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.0" - material_segmented_list: - dependency: "direct main" - description: - name: material_segmented_list - sha256: "384bfd41a78e745397ceff1dd39700961e6a5419ad911d1797bcc13ea3824241" - url: "https://pub.dev" - source: hosted - version: "1.0.5" measure_size: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7c95a5a..d371873 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,12 +62,12 @@ dependencies: media_kit_video: 2.0.1 media_kit_libs_video: 1.0.7 measure_size: ^5.0.2 - material_segmented_list: ^1.0.5 m3e_buttons: ^0.0.3 navigation_rail_m3e: git: url: https://github.com/Henry-Hiles/material_3_expressive path: packages/navigation_rail_m3e + m3e_card_list: ^0.1.0 dev_dependencies: build_runner: 2.15.0 From c5122fc34ff503eba4dd2e2ccb5820df5e8db611 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 10:55:12 -0400 Subject: [PATCH 04/11] add some more padding to member list --- lib/widgets/member_list.dart | 39 +++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index 06c0b3c..7ae560a 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -48,25 +48,28 @@ class MemberList extends HookConsumerWidget { ), ], ), - M3EToggleButtonGroup( - type: .connected, - selectedIndex: statusIndex.value, - onSelectedIndexChanged: (index) => - statusIndex.value = index ?? statusIndex.value, - // overflow: M3EButtonGroupOverflow.menu, - actions: options - .mapTo( - (name, value) => M3EToggleButtonGroupAction( - checkedLabel: Text( - "$name${switch (ref.watch(MembersByStatusController.provider(.new(roomId: roomId, status: value)))) { - AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.length})", - _ => "", - }}", + Padding( + padding: .symmetric(vertical: 8), + child: M3EToggleButtonGroup( + type: .connected, + selectedIndex: statusIndex.value, + onSelectedIndexChanged: (index) => + statusIndex.value = index ?? statusIndex.value, + // overflow: M3EButtonGroupOverflow.menu, + actions: options + .mapTo( + (name, value) => M3EToggleButtonGroupAction( + checkedLabel: Text( + "$name${switch (ref.watch(MembersByStatusController.provider(.new(roomId: roomId, status: value)))) { + AsyncData(:final value) || AsyncLoading(:final value?) => " (${value.length})", + _ => "", + }}", + ), + label: Text(name), ), - label: Text(name), - ), - ) - .toList(), + ) + .toList(), + ), ), switch (ref.watch( From 633e83b915648d52bc0776061c049bb5daa7d2b9 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 12:21:42 -0400 Subject: [PATCH 05/11] slight cleanups --- lib/widgets/member_list.dart | 1 - pubspec.lock | 2 +- pubspec.yaml | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index 7ae560a..77bada6 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -55,7 +55,6 @@ class MemberList extends HookConsumerWidget { selectedIndex: statusIndex.value, onSelectedIndexChanged: (index) => statusIndex.value = index ?? statusIndex.value, - // overflow: M3EButtonGroupOverflow.menu, actions: options .mapTo( (name, value) => M3EToggleButtonGroupAction( diff --git a/pubspec.lock b/pubspec.lock index cc7ee27..4e30e17 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -993,7 +993,7 @@ packages: source: hosted version: "2.1.5" path_provider_android: - dependency: "direct main" + dependency: transitive description: name: path_provider_android sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" diff --git a/pubspec.yaml b/pubspec.yaml index d371873..da8b1ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,7 +27,6 @@ dependencies: intl: 0.20.2 fast_immutable_collections: 11.2.0 path_provider: 2.1.5 - path_provider_android: 2.2.23 url_launcher: 6.3.2 freezed_annotation: 3.1.0 image_picker: 1.2.2 From 240984a83299affd1a5c19ad61aac181fad9b6ab Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 12:25:09 -0400 Subject: [PATCH 06/11] Don't connect M3EToggleButtonGroup I think it looks nicer this way. --- lib/widgets/member_list.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index 77bada6..c168170 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -51,7 +51,6 @@ class MemberList extends HookConsumerWidget { Padding( padding: .symmetric(vertical: 8), child: M3EToggleButtonGroup( - type: .connected, selectedIndex: statusIndex.value, onSelectedIndexChanged: (index) => statusIndex.value = index ?? statusIndex.value, From 9cc18e16b87c8e72721bddf4b8c510c3779f9493 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 13:02:51 -0400 Subject: [PATCH 07/11] only show appbar on member list if its a drawer --- lib/widgets/member_list.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/widgets/member_list.dart b/lib/widgets/member_list.dart index c168170..d3c21b6 100644 --- a/lib/widgets/member_list.dart +++ b/lib/widgets/member_list.dart @@ -34,20 +34,20 @@ class MemberList extends HookConsumerWidget { shape: Border(), child: Column( children: [ - AppBar( - scrolledUnderElevation: 0, - leading: Icon(Icons.people), - title: Text("Members"), - actionsPadding: .only(right: 4), - actions: [ - if (Scaffold.of(context).hasEndDrawer) + if (Scaffold.of(context).hasEndDrawer) + AppBar( + scrolledUnderElevation: 0, + leading: Icon(Icons.people), + title: Text("Members"), + actionsPadding: .only(right: 4), + actions: [ IconButton( onPressed: Scaffold.of(context).closeEndDrawer, icon: Icon(Icons.close), tooltip: "Close member list", ), - ], - ), + ], + ), Padding( padding: .symmetric(vertical: 8), child: M3EToggleButtonGroup( From 15c057707fd8c23dd7d3c708489f7b9010b6c278 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 13:04:12 -0400 Subject: [PATCH 08/11] slightly lower size of sidebar --- lib/widgets/sidebar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index d3f76ae..94e4cb8 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -67,7 +67,7 @@ class Sidebar extends HookConsumerWidget { .toList(); return Drawer( - width: 340, + width: 330, shape: Border(), child: Row( children: [ From 0a9b71230b2a4337921bc29e30eecff66623d03f Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 13:04:44 -0400 Subject: [PATCH 09/11] set sidebar width to infinity --- lib/widgets/sidebar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index 94e4cb8..8a6ed23 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -206,7 +206,7 @@ class Sidebar extends HookConsumerWidget { ], ), child: NavigationRailM3E( - expandedWidth: 360, + expandedWidth: double.infinity, scrollable: true, background: Colors.transparent, type: .alwaysExpand, From 0c61623a94e3581644fd335524897ceb3e7b8b86 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 13:15:41 -0400 Subject: [PATCH 10/11] fix fallback for null displayName in mention chip --- lib/widgets/html/mention_chip.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/widgets/html/mention_chip.dart b/lib/widgets/html/mention_chip.dart index 141d5a0..c757c3f 100644 --- a/lib/widgets/html/mention_chip.dart +++ b/lib/widgets/html/mention_chip.dart @@ -36,7 +36,9 @@ class MentionChip extends ConsumerWidget { child: IgnorePointer( child: Chip( label: Text( - (membership == null ? null : "@${membership.displayName}") ?? + (membership?.displayName == null + ? null + : "@${membership!.displayName}") ?? mention, style: .new( fontWeight: .bold, From e1d7a30a0649f5856c3fcd8ed2567c7451bb0c39 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Sat, 6 Jun 2026 15:11:40 -0400 Subject: [PATCH 11/11] 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), ),