diff --git a/lib/controllers/client_controller.dart b/lib/controllers/client_controller.dart index 2ebb772..bc7a904 100644 --- a/lib/controllers/client_controller.dart +++ b/lib/controllers/client_controller.dart @@ -5,6 +5,7 @@ import "package:ffi/ffi.dart"; import "package:flutter/foundation.dart"; import "package:nexus/controllers/client_state_controller.dart"; import "package:nexus/controllers/rooms_controller.dart"; +import "package:nexus/controllers/space_edges_controller.dart"; import "package:nexus/controllers/sync_status_controller.dart"; import "package:nexus/controllers/top_level_spaces_controller.dart"; import "package:nexus/helpers/extensions/gomuks_buffer.dart"; @@ -52,9 +53,16 @@ class ClientController extends AsyncNotifier { ref .watch(roomProvider.notifier) .update(syncData.rooms, syncData.leftRooms); - ref - .watch(TopLevelSpacesController.provider.notifier) - .set(syncData.topLevelSpaces); + if (syncData.topLevelSpaces != null) { + ref + .watch(TopLevelSpacesController.provider.notifier) + .set(syncData.topLevelSpaces!); + } + if (syncData.spaceEdges != null) { + ref + .watch(SpaceEdgesController.provider.notifier) + .set(syncData.spaceEdges!); + } // ref // .watch(SyncStatusController.provider.notifier) diff --git a/lib/controllers/selected_room_controller.dart b/lib/controllers/selected_room_controller.dart index cfeead6..ffba78c 100644 --- a/lib/controllers/selected_room_controller.dart +++ b/lib/controllers/selected_room_controller.dart @@ -2,24 +2,23 @@ import "package:collection/collection.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/key_controller.dart"; import "package:nexus/controllers/selected_space_controller.dart"; -import "package:nexus/models/full_room.dart"; +import "package:nexus/models/room.dart"; -class SelectedRoomController extends AsyncNotifier { +class SelectedRoomController extends Notifier { @override - Future build() async { - final space = await ref.watch(SelectedSpaceController.provider.future); + Room? build() { + final space = ref.watch(SelectedSpaceController.provider); final selectedRoomId = ref.watch( KeyController.provider(KeyController.roomKey), ); return space.children.firstWhereOrNull( - (room) => room.roomData.id == selectedRoomId, + (room) => room.metadata?.id == selectedRoomId, ) ?? space.children.firstOrNull; } - static final provider = - AsyncNotifierProvider( - SelectedRoomController.new, - ); + static final provider = NotifierProvider( + SelectedRoomController.new, + ); } diff --git a/lib/controllers/selected_space_controller.dart b/lib/controllers/selected_space_controller.dart index 75bf287..dbeb71f 100644 --- a/lib/controllers/selected_space_controller.dart +++ b/lib/controllers/selected_space_controller.dart @@ -4,12 +4,10 @@ import "package:nexus/controllers/key_controller.dart"; import "package:nexus/controllers/spaces_controller.dart"; import "package:nexus/models/space.dart"; -class SelectedSpaceController extends AsyncNotifier { +class SelectedSpaceController extends Notifier { @override - Future build() async { - final spaces = await ref.watch( - SpacesController.provider.selectAsync((data) => data), - ); + Space build() { + final spaces = ref.watch(SpacesController.provider); final selectedSpaceId = ref.watch( KeyController.provider(KeyController.spaceKey), ); @@ -18,7 +16,7 @@ class SelectedSpaceController extends AsyncNotifier { spaces.first; } - static final provider = AsyncNotifierProvider( + static final provider = NotifierProvider( SelectedSpaceController.new, ); } diff --git a/lib/controllers/space_edges_controller.dart b/lib/controllers/space_edges_controller.dart new file mode 100644 index 0000000..0349f36 --- /dev/null +++ b/lib/controllers/space_edges_controller.dart @@ -0,0 +1,15 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:nexus/models/space_edge.dart"; + +class SpaceEdgesController extends Notifier>> { + @override + IMap> build() => const IMap.empty(); + + void set(IMap> newEdges) => state = newEdges; + + static final provider = + NotifierProvider>>( + SpaceEdgesController.new, + ); +} diff --git a/lib/controllers/spaces_controller.dart b/lib/controllers/spaces_controller.dart index e269584..a62c55c 100644 --- a/lib/controllers/spaces_controller.dart +++ b/lib/controllers/spaces_controller.dart @@ -1,55 +1,94 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:nexus/controllers/client_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/space.dart"; +import "package:nexus/models/room.dart"; +import "package:nexus/models/space_edge.dart"; -class SpacesController extends AsyncNotifier> { +class SpacesController extends Notifier> { @override - Future> build() async { - final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider); + IList build() { final rooms = ref.watch(RoomsController.provider); + final topLevelSpaceIds = ref.watch(TopLevelSpacesController.provider); + final spaceEdges = ref.watch(SpaceEdgesController.provider); - final topLevelSpaces = topLevelSpaceIds - .map((id) => rooms[id]) - .nonNulls - .toIList(); + ISet collectChildIds(String spaceId) { + ISet result = ISet(); + void walk(String currentId) { + final children = spaceEdges[currentId] ?? IList(); + for (final edge in children) { + final childId = edge.childId; + if (!result.contains(childId)) { + result = result.add(childId); + walk(childId); + } + } + } + + walk(spaceId); + return result; + } + + final spaceIdToChildren = IMap.fromEntries( + topLevelSpaceIds.map((spaceId) { + final children = collectChildIds( + spaceId, + ).map((id) => rooms[id]).nonNulls.toIList(); + return MapEntry(spaceId, children); + }), + ); + + final allNestedRoomIds = spaceIdToChildren.values + .expand((l) => l) + .map((r) => rooms.entries.firstWhere((e) => e.value == r).key) + .toISet(); final dmRooms = rooms.values .where((room) => room.metadata?.dmUserId != null) .toIList(); - final topLevelRooms = rooms.values - .where((room) => room.metadata?.dmUserId == null) + final homeRooms = rooms.entries .where( - (room) => spaceRooms.every( - (space) => - space.spaceChildren.every((child) => child.roomId != room.id), - ), + (e) => + e.value.metadata?.dmUserId == null && + !allNestedRoomIds.contains(e.key) && + !topLevelSpaceIds.contains(e.key), ) + .map((e) => e.value) .toIList(); - // 4️⃣ Combine all into a single IList - return IList([ - Space( - id: "home", - title: "Home", - children: topLevelRooms, - icon: Icons.home, - ), + final topLevelSpacesList = topLevelSpaceIds + .map((id) { + final room = rooms[id]; + if (room == null) return null; + + final children = spaceIdToChildren[id] ?? IList(); + return Space( + id: id, + title: room.metadata?.name ?? "Unnamed Room", + room: room, + children: children, + ); + }) + .nonNulls + .toIList(); + + return [ + Space(id: "home", title: "Home", icon: Icons.home, children: homeRooms), Space( id: "dms", title: "Direct Messages", + icon: Icons.people, children: dmRooms, - icon: Icons.person, ), - ...topLevelSpaces, - ]); + ...topLevelSpacesList, + ].toIList(); } - static final provider = AsyncNotifierProvider>( + static final provider = NotifierProvider>( SpacesController.new, ); } diff --git a/lib/helpers/extensions/join_room_with_snackbars.dart b/lib/helpers/extensions/join_room_with_snackbars.dart index 0c7b1b3..df89740 100644 --- a/lib/helpers/extensions/join_room_with_snackbars.dart +++ b/lib/helpers/extensions/join_room_with_snackbars.dart @@ -1,87 +1,87 @@ import "package:collection/collection.dart"; import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; -import "package:matrix/matrix.dart"; +import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/key_controller.dart"; import "package:nexus/controllers/spaces_controller.dart"; -extension JoinRoomWithSnackbars on Client { +extension JoinRoomWithSnackbars on ClientController { Future joinRoomWithSnackBars( BuildContext context, String roomAlias, WidgetRef ref, ) async { - final parsed = roomAlias.parseIdentifierIntoParts(); - final alias = parsed?.primaryIdentifier ?? roomAlias; + // final parsed = roomAlias.parseIdentifierIntoParts(); + // final alias = parsed?.primaryIdentifier ?? roomAlias; - final scaffoldMessenger = ScaffoldMessenger.of(context); + // final scaffoldMessenger = ScaffoldMessenger.of(context); - final snackbar = scaffoldMessenger.showSnackBar( - SnackBar( - content: Text("Joining room $alias."), - duration: Duration(days: 999), - ), - ); + // final snackbar = scaffoldMessenger.showSnackBar( + // SnackBar( + // content: Text("Joining room $alias."), + // duration: Duration(days: 999), + // ), + // ); - try { - final id = await joinRoom(alias, via: parsed?.via.toList()); + // try { + // final id = await joinRoom(alias, via: parsed?.via.toList()); - snackbar.close(); + // snackbar.close(); - scaffoldMessenger.showSnackBar( - SnackBar( - content: Text("Room $alias successfully joined."), - action: SnackBarAction( - label: "Open", - onPressed: () async { - final spaces = await ref.refresh( - SpacesController.provider.future, - ); - final space = spaces.firstWhereOrNull((space) => space.id == id); + // scaffoldMessenger.showSnackBar( + // SnackBar( + // content: Text("Room $alias successfully joined."), + // action: SnackBarAction( + // label: "Open", + // onPressed: () async { + // final spaces = await ref.refresh( + // SpacesController.provider.future, + // ); + // final space = spaces.firstWhereOrNull((space) => space.id == id); - await ref - .watch( - KeyController.provider(KeyController.spaceKey).notifier, - ) - .set( - space?.id ?? - spaces - .firstWhere( - (space) => - space.children.firstWhereOrNull( - (child) => child.roomData.id == id, - ) != - null, - ) - .id, - ); + // await ref + // .watch( + // KeyController.provider(KeyController.spaceKey).notifier, + // ) + // .set( + // space?.id ?? + // spaces + // .firstWhere( + // (space) => + // space.children.firstWhereOrNull( + // (child) => child.roomData.id == id, + // ) != + // null, + // ) + // .id, + // ); - if (space == null) { - await ref - .watch( - KeyController.provider(KeyController.roomKey).notifier, - ) - .set(id); - } - }, - ), - ), - ); - } catch (error) { - snackbar.close(); - if (context.mounted) { - scaffoldMessenger.showSnackBar( - SnackBar( - backgroundColor: Theme.of(context).colorScheme.errorContainer, - content: Text( - error.toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.onErrorContainer, - ), - ), - ), - ); - } - } + // if (space == null) { + // await ref + // .watch( + // KeyController.provider(KeyController.roomKey).notifier, + // ) + // .set(id); + // } + // }, + // ), + // ), + // ); + // } catch (error) { + // snackbar.close(); + // if (context.mounted) { + // scaffoldMessenger.showSnackBar( + // SnackBar( + // backgroundColor: Theme.of(context).colorScheme.errorContainer, + // content: Text( + // error.toString(), + // style: TextStyle( + // color: Theme.of(context).colorScheme.onErrorContainer, + // ), + // ), + // ), + // ); + // } + // } } } diff --git a/lib/models/space.dart b/lib/models/space.dart index 15b818c..631759a 100644 --- a/lib/models/space.dart +++ b/lib/models/space.dart @@ -1,3 +1,4 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/widgets.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/room.dart"; @@ -10,6 +11,6 @@ abstract class Space with _$Space { required String title, IconData? icon, Room? room, - required List children, + required IList children, }) = _Space; } diff --git a/lib/models/space_edge.dart b/lib/models/space_edge.dart new file mode 100644 index 0000000..192af31 --- /dev/null +++ b/lib/models/space_edge.dart @@ -0,0 +1,14 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +part "space_edge.freezed.dart"; +part "space_edge.g.dart"; + +@freezed +abstract class SpaceEdge with _$SpaceEdge { + const factory SpaceEdge({ + required String childId, + @Default(false) bool suggested, + }) = _SpaceEdge; + + factory SpaceEdge.fromJson(Map json) => + _$SpaceEdgeFromJson(json); +} diff --git a/lib/models/sync_data.dart b/lib/models/sync_data.dart index 31f69e3..0fc18ac 100644 --- a/lib/models/sync_data.dart +++ b/lib/models/sync_data.dart @@ -1,6 +1,7 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:freezed_annotation/freezed_annotation.dart"; import "package:nexus/models/room.dart"; +import "package:nexus/models/space_edge.dart"; part "sync_data.freezed.dart"; part "sync_data.g.dart"; @@ -12,8 +13,8 @@ abstract class SyncData with _$SyncData { @Default(IMap.empty()) IMap rooms, @Default(ISet.empty()) ISet leftRooms, // required IList invitedRooms, - // required IList spaceEdges, - @Default(IList.empty()) IList topLevelSpaces, + IMap>? spaceEdges, + IList? topLevelSpaces, }) = _SyncData; factory SyncData.fromJson(Map json) => diff --git a/lib/pages/chat_page.dart b/lib/pages/chat_page.dart index 23f6a09..0899605 100644 --- a/lib/pages/chat_page.dart +++ b/lib/pages/chat_page.dart @@ -1,6 +1,6 @@ import "package:flutter/material.dart"; +import "package:nexus/widgets/chat_page/sidebar.dart"; // import "package:nexus/widgets/chat_page/room_chat.dart"; -// import "package:nexus/widgets/chat_page/sidebar.dart"; class ChatPage extends StatelessWidget { const ChatPage({super.key}); @@ -10,23 +10,23 @@ class ChatPage extends StatelessWidget { builder: (context, constraints) { final isDesktop = constraints.maxWidth > 650; final showMembersByDefault = constraints.maxWidth > 1000; - return Placeholder(); - // return Scaffold( - // body: Builder( - // builder: (context) => Row( - // children: [ - // if (isDesktop) Sidebar(), - // Expanded( - // child: RoomChat( - // isDesktop: isDesktop, - // showMembersByDefault: showMembersByDefault, - // ), - // ), - // ], - // ), - // ), - // drawer: isDesktop ? null : Sidebar(), - // ); + + return Scaffold( + body: Builder( + builder: (context) => Row( + children: [ + if (isDesktop) Sidebar(), + // Expanded( + // child: RoomChat( + // isDesktop: isDesktop, + // showMembersByDefault: showMembersByDefault, + // ), + // ), + ], + ), + ), + drawer: isDesktop ? null : Sidebar(), + ); }, ); } diff --git a/lib/widgets/appbar.dart b/lib/widgets/appbar.dart index fa2088d..00b0e4c 100644 --- a/lib/widgets/appbar.dart +++ b/lib/widgets/appbar.dart @@ -1,4 +1,5 @@ import "dart:io"; +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:window_manager/window_manager.dart"; @@ -7,7 +8,7 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget { final Widget? title; final Color? backgroundColor; final double? scrolledUnderElevation; - final List actions; + final IList actions; const Appbar({ super.key, @@ -15,7 +16,7 @@ class Appbar extends StatelessWidget implements PreferredSizeWidget { this.backgroundColor, this.scrolledUnderElevation, this.leading, - this.actions = const [], + this.actions = const IList.empty(), }); @override diff --git a/lib/widgets/chat_page/room_menu.dart b/lib/widgets/chat_page/room_menu.dart index ea123fd..2342a20 100644 --- a/lib/widgets/chat_page/room_menu.dart +++ b/lib/widgets/chat_page/room_menu.dart @@ -1,122 +1,124 @@ +import "package:fast_immutable_collections/fast_immutable_collections.dart"; import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_hooks/flutter_hooks.dart"; -import "package:matrix/matrix.dart"; -import "package:nexus/helpers/extensions/room_to_children.dart"; +import "package:nexus/models/room.dart"; import "package:nexus/widgets/form_text_input.dart"; class RoomMenu extends StatelessWidget { final Room room; - const RoomMenu(this.room, {super.key}); + final IList children; + const RoomMenu(this.room, {this.children = const IList.empty(), super.key}); @override Widget build(BuildContext context) { final danger = Theme.of(context).colorScheme.error; void markRead(String roomId) async { - for (final child in await room.getAllChildren()) { - await child.roomData.setReadMarker( - child.roomData.lastEvent?.eventId, - mRead: child.roomData.lastEvent?.eventId, - ); + // TODO: Set parent read + for (final child in children) { + // await child.setReadMarker( TODO: Set children read + // child.roomData.lastEvent?.eventId, + // mRead: child.roomData.lastEvent?.eventId, + // ); } } return PopupMenuButton( itemBuilder: (_) => [ - PopupMenuItem( - onTap: () async { - final link = await room.matrixToInviteLink(); - await Clipboard.setData(ClipboardData(text: link.toString())); - }, - child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), - ), - PopupMenuItem( - onTap: () => markRead(room.id), - child: ListTile( - leading: Icon(Icons.check), - title: Text("Mark as Read"), - ), - ), - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text("Leave Room"), - content: Text( - "Are you sure you want to leave \"${room.getLocalizedDisplayname()}\"?", - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - final snackbar = ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text("Leaving room..."))); - await room.leave(); - snackbar.close(); - }, - child: Text("Leave"), - ), - ], - ), - ), - child: ListTile( - leading: Icon(Icons.logout, color: danger), - title: Text("Leave", style: TextStyle(color: danger)), - ), - ), - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (context) => HookBuilder( - builder: (_) { - final reasonController = useTextEditingController(); - return AlertDialog( - title: Text("Report"), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Report this room to your server administrators, who can take action like banning this room.", - ), + // PopupMenuItem( + // onTap: () async { + // final link = await room.matrixToInviteLink(); + // await Clipboard.setData(ClipboardData(text: link.toString())); + // }, + // child: ListTile(leading: Icon(Icons.link), title: Text("Copy Link")), + // ), + // PopupMenuItem( + // onTap: () => markRead(room.id), + // child: ListTile( + // leading: Icon(Icons.check), + // title: Text("Mark as Read"), + // ), + // ), + // PopupMenuItem( + // onTap: () => showDialog( + // context: context, + // builder: (context) => AlertDialog( + // title: Text("Leave Room"), + // content: Text( + // "Are you sure you want to leave \"${room.getLocalizedDisplayname()}\"?", + // ), + // actions: [ + // TextButton( + // onPressed: Navigator.of(context).pop, + // child: Text("Cancel"), + // ), + // TextButton( + // onPressed: () async { + // Navigator.of(context).pop(); + // final snackbar = ScaffoldMessenger.of( + // context, + // ).showSnackBar(SnackBar(content: Text("Leaving room..."))); + // await room.leave(); + // snackbar.close(); + // }, + // child: Text("Leave"), + // ), + // ], + // ), + // ), + // child: ListTile( + // leading: Icon(Icons.logout, color: danger), + // title: Text("Leave", style: TextStyle(color: danger)), + // ), + // ), + // PopupMenuItem( + // onTap: () => showDialog( + // context: context, + // builder: (context) => HookBuilder( + // builder: (_) { + // final reasonController = useTextEditingController(); + // return AlertDialog( + // title: Text("Report"), + // content: Column( + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // "Report this room to your server administrators, who can take action like banning this room.", + // ), - SizedBox(height: 12), - FormTextInput( - required: false, - capitalize: true, - controller: reasonController, - title: "Reason for report (optional)", - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () { - room.client.reportRoom(room.id, reasonController.text); - Navigator.of(context).pop(); - }, - child: Text("Report"), - ), - ], - ); - }, - ), - ), - child: ListTile( - leading: Icon(Icons.report, color: danger), - title: Text("Report", style: TextStyle(color: danger)), - ), - ), + // SizedBox(height: 12), + // FormTextInput( + // required: false, + // capitalize: true, + // controller: reasonController, + // title: "Reason for report (optional)", + // ), + // ], + // ), + // actions: [ + // TextButton( + // onPressed: Navigator.of(context).pop, + // child: Text("Cancel"), + // ), + // TextButton( + // onPressed: () { + // room.client.reportRoom(room.id, reasonController.text); + // Navigator.of(context).pop(); + // }, + // child: Text("Report"), + // ), + // ], + // ); + // }, + // ), + // ), + // child: ListTile( + // leading: Icon(Icons.report, color: danger), + // title: Text("Report", style: TextStyle(color: danger)), + // ), + // ), ], ); } diff --git a/lib/widgets/chat_page/sidebar.dart b/lib/widgets/chat_page/sidebar.dart index a5e1425..3a59b41 100644 --- a/lib/widgets/chat_page/sidebar.dart +++ b/lib/widgets/chat_page/sidebar.dart @@ -1,4 +1,3 @@ -import "package:collection/collection.dart"; import "package:flutter/material.dart"; import "package:flutter_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; @@ -6,8 +5,6 @@ import "package:nexus/controllers/client_controller.dart"; import "package:nexus/controllers/key_controller.dart"; import "package:nexus/controllers/selected_space_controller.dart"; import "package:nexus/controllers/spaces_controller.dart"; -import "package:nexus/helpers/extensions/better_when.dart"; -import "package:nexus/helpers/extensions/get_headers.dart"; import "package:nexus/helpers/extensions/join_room_with_snackbars.dart"; import "package:nexus/pages/settings_page.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; @@ -22,228 +19,208 @@ class Sidebar extends HookConsumerWidget { final selectedSpaceProvider = KeyController.provider( KeyController.spaceKey, ); - final selectedSpace = ref.watch(selectedSpaceProvider); - final selectedSpaceNotifier = ref.watch(selectedSpaceProvider.notifier); + final selectedSpaceId = ref.watch(selectedSpaceProvider); + final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier); final selectedRoomController = KeyController.provider( KeyController.roomKey, ); - final selectedRoom = ref.watch(selectedRoomController); - final selectedRoomNotifier = ref.watch(selectedRoomController.notifier); + final selectedRoomId = ref.watch(selectedRoomController); + final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier); + + final spaces = ref.watch(SpacesController.provider); + final indexOfSelected = spaces.indexWhere( + (space) => space.id == selectedSpaceId, + ); + final selectedIndex = indexOfSelected == -1 ? 0 : indexOfSelected; + + final selectedSpace = ref.watch(SelectedSpaceController.provider); + + final indexOfSelectedRoom = selectedSpace.children.indexWhere( + (room) => room.metadata?.id == selectedRoomId, + ); + final selectedRoomIndex = indexOfSelected == -1 + ? selectedSpace.children.isEmpty + ? null + : 0 + : indexOfSelectedRoom; return Drawer( shape: Border(), child: Row( children: [ - ref - .watch(SpacesController.provider) - .when( - loading: SizedBox.shrink, - error: (error, stack) { - debugPrintStack(label: error.toString(), stackTrace: stack); - throw error; - }, - data: (spaces) { - final indexOfSelected = spaces.indexWhere( - (space) => space.id == selectedSpace, - ); - final selectedIndex = indexOfSelected == -1 - ? 0 - : indexOfSelected; - - return NavigationRail( - scrollable: true, - onDestinationSelected: (value) { - selectedSpaceNotifier.set(spaces[value].id); - selectedRoomNotifier.set( - spaces[value].children.firstOrNull?.roomData.id, - ); - }, - destinations: spaces - .map( - (space) => NavigationRailDestination( - icon: AvatarOrHash( - space.avatar, - fallback: space.icon == null - ? null - : Icon(space.icon), - space.title, - headers: space.client.headers, - hasBadge: - space.children.firstWhereOrNull( - (room) => room.roomData.hasNewMessages, - ) != - null, - ), - label: Text(space.title), - padding: EdgeInsets.only(top: 4), - ), - ) - .toList(), - selectedIndex: selectedIndex, - trailingAtBottom: true, - trailing: Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Column( - spacing: 8, - children: [ - PopupMenuButton( - itemBuilder: (_) => [ - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (alertContext) => HookBuilder( - builder: (_) { - final roomAlias = - useTextEditingController(); - return AlertDialog( - title: Text("Join a Room"), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - "Enter the room alias, ID, or a Matrix.to link.", - ), - SizedBox(height: 12), - FormTextInput( - required: false, - capitalize: true, - controller: roomAlias, - title: "#room:server", - ), - ], - ), - actions: [ - TextButton( - onPressed: Navigator.of( - context, - ).pop, - child: Text("Cancel"), - ), - TextButton( - onPressed: () async { - Navigator.of(alertContext).pop(); - - final client = await ref.watch( - ClientController - .provider - .future, - ); - if (context.mounted) { - client.joinRoomWithSnackBars( - context, - roomAlias.text, - ref, - ); - } - }, - child: Text("Join"), - ), - ], - ); - }, - ), - ), - child: ListTile( - title: Text( - "Join an existing room (or space)", - ), - leading: Icon(Icons.numbers), - ), - ), - PopupMenuItem( - onTap: () {}, - child: ListTile( - title: Text("Create a new room"), - leading: Icon(Icons.add), - ), - ), - ], - icon: Icon(Icons.add), - ), - IconButton( - onPressed: () => showDialog( - context: context, - builder: (context) => - AlertDialog(title: Text("To-do")), - ), - icon: Icon(Icons.explore), - ), - IconButton( - onPressed: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => SettingsPage()), - ), - icon: Icon(Icons.settings), - ), - ], - ), + NavigationRail( + scrollable: true, + onDestinationSelected: (value) { + selectedSpaceIdNotifier.set(spaces[value].id); + selectedRoomIdNotifier.set( + spaces[value].children.firstOrNull?.metadata?.id, + ); + }, + destinations: spaces + .map( + (space) => NavigationRailDestination( + icon: AvatarOrHash( + null, // TODO: Url + fallback: space.icon == null ? null : Icon(space.icon), + space.title, + headers: {}, // TODO + hasBadge: false, + // space.children.firstWhereOrNull( TODO + // (room) => room.roomData.hasNewMessages, + // ) != + // null, ), - ); - }, - ), - Expanded( - child: ref - .watch(SelectedSpaceController.provider) - .betterWhen( - data: (space) { - final indexOfSelected = space.children.indexWhere( - (room) => room.roomData.id == selectedRoom, - ); - final selectedIndex = indexOfSelected == -1 - ? space.children.isEmpty - ? null - : 0 - : indexOfSelected; - - return Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - leading: AvatarOrHash( - space.avatar, - fallback: space.icon == null - ? null - : Icon(space.icon), - space.title, - headers: space.client.headers, - ), - title: Text( - space.title, - overflow: TextOverflow.ellipsis, - ), - backgroundColor: Colors.transparent, - actions: [ - if (space.roomData != null) RoomMenu(space.roomData!), - ], - ), - body: NavigationRail( - scrollable: true, - backgroundColor: Colors.transparent, - extended: true, - selectedIndex: selectedIndex, - destinations: space.children - .map( - (room) => NavigationRailDestination( - label: Text(room.title), - icon: AvatarOrHash( - hasBadge: room.roomData.hasNewMessages, - room.avatar, - room.title, - fallback: selectedSpace == "dms" - ? null - : Icon(Icons.numbers), - headers: space.client.headers, + label: Text(space.title), + padding: EdgeInsets.only(top: 4), + ), + ) + .toList(), + selectedIndex: selectedIndex, + trailingAtBottom: true, + trailing: Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Column( + spacing: 8, + children: [ + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (alertContext) => HookBuilder( + builder: (_) { + final roomAlias = useTextEditingController(); + return AlertDialog( + title: Text("Join a Room"), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Enter the room alias, ID, or a Matrix.to link.", + ), + SizedBox(height: 12), + FormTextInput( + required: false, + capitalize: true, + controller: roomAlias, + title: "#room:server", + ), + ], ), - ), - ) - .toList(), - onDestinationSelected: (value) => selectedRoomNotifier - .set(space.children[value].roomData.id), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text("Cancel"), + ), + TextButton( + onPressed: () async { + Navigator.of(alertContext).pop(); + + final client = ref.watch( + ClientController.provider.notifier, + ); + if (context.mounted) { + client.joinRoomWithSnackBars( + context, + roomAlias.text, + ref, + ); + } + }, + child: Text("Join"), + ), + ], + ); + }, + ), + ), + child: ListTile( + title: Text("Join an existing room (or space)"), + leading: Icon(Icons.numbers), + ), ), - ); - }, + PopupMenuItem( + onTap: () {}, + child: ListTile( + title: Text("Create a new room"), + leading: Icon(Icons.add), + ), + ), + ], + icon: Icon(Icons.add), + ), + IconButton( + onPressed: () => showDialog( + context: context, + builder: (context) => AlertDialog(title: Text("To-do")), + ), + icon: Icon(Icons.explore), + ), + IconButton( + onPressed: () => Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => SettingsPage())), + icon: Icon(Icons.settings), + ), + ], + ), + ), + ), + Expanded( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: AvatarOrHash( + null, + // space.avatar, TODO + fallback: selectedSpace.icon == null + ? null + : Icon(selectedSpace.icon), + + selectedSpace.title, // TODO RM + headers: {}, + // space.client.headers, TODO ), + title: Text( + selectedSpace.room?.metadata?.avatar.toString() ?? + selectedSpace.title, + overflow: TextOverflow.ellipsis, + ), + backgroundColor: Colors.transparent, + actions: [ + if (selectedSpace.room != null) RoomMenu(selectedSpace.room!), + ], + ), + body: NavigationRail( + scrollable: true, + backgroundColor: Colors.transparent, + extended: true, + selectedIndex: selectedRoomIndex, + destinations: selectedSpace.children + .map( + (room) => NavigationRailDestination( + label: Text(room.metadata?.name ?? "Unnamed Room"), + icon: AvatarOrHash( + // hasBadge: room.roomData.hasNewMessages, TODO + null, + // room.avatar, TODO + room.metadata?.name ?? "Unnamed Room", + fallback: selectedSpaceId == "dms" + ? null + : Icon(Icons.numbers), + headers: {}, + // space.client.headers, + ), + ), + ) + .toList(), + onDestinationSelected: (value) => selectedRoomIdNotifier.set( + selectedSpace.children[value].metadata?.id, + ), + ), + ), ), ], ),