diff --git a/lib/helpers/extensions/get_all_child_rooms.dart b/lib/helpers/extensions/get_all_child_rooms.dart deleted file mode 100644 index 7a17eec..0000000 --- a/lib/helpers/extensions/get_all_child_rooms.dart +++ /dev/null @@ -1,8 +0,0 @@ -import "package:fast_immutable_collections/fast_immutable_collections.dart"; -import "package:nexus/models/room.dart"; -import "package:nexus/models/space.dart"; - -extension GetAllChildRooms on Space { - IList get allChildRooms => - children.addAll(subSpaces.expand((s) => s.children)); -} diff --git a/lib/helpers/mxc_image.dart b/lib/helpers/mxc_image.dart index c2f3a0c..83a345a 100644 --- a/lib/helpers/mxc_image.dart +++ b/lib/helpers/mxc_image.dart @@ -1,6 +1,5 @@ import "dart:ui"; -import "package:flutter/foundation.dart"; import "package:flutter/widgets.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; @@ -10,18 +9,19 @@ class MxcImage(final WidgetRef ref, final DownloadMediaRequest request) extends ImageProvider { @override Future obtainKey(ImageConfiguration configuration) => - SynchronousFuture(this); + Future.value(this); @override ImageStreamCompleter loadImage(MxcImage key, ImageDecoderCallback decode) => - MultiFrameImageStreamCompleter(codec: _loadAsync(decode), scale: 1); + MultiFrameImageStreamCompleter(codec: _loadAsync(decode), scale: 1.0); Future _loadAsync(ImageDecoderCallback decode) async { final file = await ref .read(ClientController.provider.notifier) .downloadMedia(request); + final buffer = await ImmutableBuffer.fromFilePath(file.path); - return decode(await ImmutableBuffer.fromFilePath(file.path)); + return decode(buffer); } @override diff --git a/lib/widgets/room_list.dart b/lib/widgets/room_list.dart index 8b96cbf..d65ba47 100644 --- a/lib/widgets/room_list.dart +++ b/lib/widgets/room_list.dart @@ -1,9 +1,6 @@ -import "dart:math"; - import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:material_ui/material_ui.dart"; import "package:navigation_rail_m3e/navigation_rail_m3e.dart"; -import "package:nexus/helpers/extensions/get_all_child_rooms.dart"; import "package:nexus/models/nav_page.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/divider_widget.dart"; @@ -20,26 +17,9 @@ import "package:fast_immutable_collections/fast_immutable_collections.dart"; // Needed for navigation_rail_m3e (#65). import "package:flutter/material.dart" as old_mat; -List roomDestinations( - IList rooms, { - required bool isDm, -}) => [ - for (final room in rooms) - .new( - label: room.metadata?.name ?? "Unnamed Room", - badgeCount: switch (room.metadata?.unreadNotifications) { - 0 || null => room.metadata?.unreadMessages == 0 ? null : 0, - int unread => unread, - }, - icon: AvatarOrHash( - room.metadata?.avatar, - room.metadata?.name ?? "Unnamed Room", - fallback: isDm ? null : const Icon(Icons.numbers), - ), - ), -]; - -class const RoomList({super.key}) extends ConsumerWidget implements NavPage { +class const RoomList({super.key}) + extends HookConsumerWidget + implements NavPage { @override String get title => "Rooms"; @@ -52,8 +32,21 @@ class const RoomList({super.key}) extends ConsumerWidget implements NavPage { @override Widget build(BuildContext context, WidgetRef ref) { + final selectedSpaceProvider = KeyController.provider( + KeyController.spaceKey, + ); + final selectedSpaceId = ref.watch(selectedSpaceProvider).requireValue; + final selectedSpaceIdNotifier = ref.watch(selectedSpaceProvider.notifier); + + final selectedRoomController = KeyController.provider( + KeyController.roomKey, + ); + final selectedRoomId = ref.watch(selectedRoomController).requireValue; + final selectedRoomIdNotifier = ref.watch(selectedRoomController.notifier); + final spacesAsync = ref.watch(SpacesController.provider); - if (spacesAsync.value == null) { + final spaces = spacesAsync.value; + if (spaces == null) { return switch (spacesAsync) { AsyncError(:final error, :final stackTrace) => ErrorDialog( error, @@ -63,253 +56,271 @@ class const RoomList({super.key}) extends ConsumerWidget implements NavPage { }; } - return const Row( + final indexOfSelected = spaces.indexWhere( + (space) => space.id == selectedSpaceId, + ); + + final selectedIndex = indexOfSelected == -1 ? 0 : indexOfSelected; + + final selectedSpace = + spaces.firstWhereOrNull((space) => space.id == selectedSpaceId) ?? + spaces.first; + + final indexOfSelectedRoom = selectedSpace.children + .addAll( + selectedSpace.subSpaces.map((element) => element.children).flattened, + ) + .indexWhere((room) => room.metadata?.id == selectedRoomId); + final selectedRoomIndex = indexOfSelectedRoom == -1 + ? null + : indexOfSelectedRoom; + + List roomsToDestinations(IList rooms) => + rooms + .map( + (room) => NavigationRailM3EDestination( + label: room.metadata?.name ?? "Unnamed Room", + badgeCount: switch (room.metadata?.unreadNotifications) { + 0 || null => room.metadata?.unreadMessages == 0 ? null : 0, + int unread => unread, + }, + icon: AvatarOrHash( + room.metadata?.avatar, + room.metadata?.name ?? "Unnamed Room", + fallback: selectedSpaceId == "dms" + ? null + : Icon(Icons.numbers), + ), + ), + ) + .toList(); + + return Row( children: [ - SpacesRail(), - Expanded(child: RoomsPane()), + MaterialUiCompatibilityBridge( + child: Builder( + builder: (context) => old_mat.Theme( + data: old_mat.Theme.of(context).copyWith( + extensions: [ + NavigationRailM3ETheme( + itemCollapsedHeight: 48, + itemVerticalGap: 0, + ), + ], + ), + child: Container( + color: NavigationRailTokensAdapter(context).containerColor, + padding: EdgeInsets.only(top: 16), + child: NavigationRailM3E( + type: .alwaysCollapse, + labelBehavior: .alwaysHide, + scrollable: true, + onDestinationSelected: (value) { + selectedSpaceIdNotifier.set(spaces[value].id); + selectedRoomIdNotifier.set( + spaces[value].children.firstOrNull?.metadata?.id, + ); + }, + sections: [ + .new( + destinations: spaces + .map( + (space) => NavigationRailM3EDestination( + badgeCount: switch (space.children + .addAll( + space.subSpaces + .map((element) => element.children) + .flattened, + ) + .fold( + 0, + (previousValue, room) => + previousValue + + (room.metadata?.unreadNotifications ?? + 0), + )) { + 0 => + space.children + .addAll( + space.subSpaces + .map( + (element) => element.children, + ) + .flattened, + ) + .any( + (room) => + room.metadata?.unreadMessages != + 0, + ) + ? 0 + : null, + int badgeCount => badgeCount, + }, + short: true, + icon: AvatarOrHash( + dimension: 28, + space.room?.metadata?.avatar, + fallback: space.icon == null + ? null + : Icon(space.icon), + space.title, + ), + label: space.title, + ), + ) + .toList(), + ), + ], + selectedIndex: selectedIndex, + trailingAtBottom: true, + trailing: Padding( + padding: .only(top: 8), + child: Column( + children: [ + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + onTap: () => showDialog( + context: context, + builder: (_) => JoinDialog(ref), + ), + child: ListTile( + title: Text("Join an existing room (or space)"), + leading: Icon(Icons.numbers), + ), + ), + PopupMenuItem( + onTap: null, + child: ListTile( + title: Text("Create a new room"), + leading: Icon(Icons.add), + ), + ), + ], + icon: Icon(Icons.add), + ), + IconButton( + tooltip: "Explore other rooms", + onPressed: null, + icon: Icon(Icons.explore), + ), + IconButton( + tooltip: "Open settings", + onPressed: () => showDialog( + context: context, + builder: (_) => SettingsPage(), + ), + icon: Icon(Icons.settings), + ), + ], + ), + ), + ), + ), + ), + ), + ), + + Expanded( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + leading: AvatarOrHash( + selectedSpace.room?.metadata?.avatar, + fallback: selectedSpace.icon == null + ? null + : Icon(selectedSpace.icon), + + selectedSpace.title, + ), + title: Text(selectedSpace.title, overflow: .ellipsis), + backgroundColor: Colors.transparent, + actions: [ + RoomMenu( + selectedSpace.room, + children: selectedSpace.children.addAll( + selectedSpace.subSpaces + .map((element) => element.children) + .flattened, + ), + ), + ], + ), + body: MaterialUiCompatibilityBridge( + child: Builder( + builder: (context) => old_mat.Theme( + data: old_mat.Theme.of(context).copyWith( + extensions: [ + NavigationRailM3ETheme( + itemExpandedHeight: 48, + iconLabelGap: 16, + ), + ], + ), + child: NavigationRailM3E( + expandedWidth: double.infinity, + scrollable: true, + background: Colors.transparent, + type: .alwaysExpand, + selectedIndex: selectedRoomIndex ?? 0, + sections: [ + .new( + header: selectedSpace.room == null + ? null + : DividerWidget(Text("Rooms")), + destinations: roomsToDestinations( + selectedSpace.children, + ), + ), + for (final subSpace in selectedSpace.subSpaces) + .new( + header: DividerWidget( + Row( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + if (subSpace.room.metadata?.avatar != null) + AvatarOrHash( + subSpace.room.metadata?.avatar, + subSpace.room.metadata?.name ?? + "Unnamed Room", + dimension: 16, + ), + Flexible( + child: Text( + subSpace.room.metadata?.name ?? + "Unnamed Space", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + destinations: roomsToDestinations(subSpace.children), + ), + ], + onDestinationSelected: (value) { + final children = selectedSpace.children.addAll( + selectedSpace.subSpaces + .map((element) => element.children) + .flattened, + ); + selectedRoomIdNotifier.set( + children[value].metadata?.id, // + ); + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + }, + ), + ), + ), + ), + ), + ), ], ); } } - -class const SpacesRail({super.key}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final spaces = ref.watch( - SpacesController.provider.select((async) => async.requireValue), - ); - final selectedSpaceId = ref.watch( - KeyController.provider(KeyController.spaceKey) - .select((async) => async.requireValue), - ); - - return MaterialUiCompatibilityBridge( - child: Builder( - builder: (context) => old_mat.Theme( - data: old_mat.Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemCollapsedHeight: 48, - itemVerticalGap: 0, - ), - ], - ), - child: Container( - color: NavigationRailTokensAdapter(context).containerColor, - padding: .only(top: 16), - child: NavigationRailM3E( - type: .alwaysCollapse, - labelBehavior: .alwaysHide, - scrollable: true, - onDestinationSelected: (value) { - ref - .read( - KeyController.provider(KeyController.spaceKey).notifier, - ) - .set(spaces[value].id); - ref - .read( - KeyController.provider(KeyController.roomKey).notifier, - ) - .set(spaces[value].children.firstOrNull?.metadata?.id); - }, - sections: [ - .new( - destinations: spaces.map((space) { - final notifications = space.allChildRooms.fold( - 0, - (sum, room) => - sum + (room.metadata?.unreadNotifications ?? 0), - ); - - return NavigationRailM3EDestination( - badgeCount: notifications > 0 - ? notifications - : space.allChildRooms.any( - (room) => room.metadata?.unreadMessages != 0, - ) - ? 0 - : null, - short: true, - icon: AvatarOrHash( - dimension: 28, - space.room?.metadata?.avatar, - fallback: space.icon == null ? null : Icon(space.icon), - space.title, - ), - label: space.title, - ); - }).toList(), - ), - ], - selectedIndex: max( - 0, - spaces.indexWhere((space) => space.id == selectedSpaceId), - ), - trailingAtBottom: true, - trailing: Padding( - padding: .only(top: 8), - child: Column( - children: [ - PopupMenuButton( - itemBuilder: (_) => [ - PopupMenuItem( - onTap: () => showDialog( - context: context, - builder: (_) => JoinDialog(ref), - ), - child: const ListTile( - title: Text("Join an existing room (or space)"), - leading: Icon(Icons.numbers), - ), - ), - const PopupMenuItem( - onTap: null, - child: ListTile( - title: Text("Create a new room"), - leading: Icon(Icons.add), - ), - ), - ], - icon: const Icon(Icons.add), - ), - const IconButton( - tooltip: "Explore other rooms", - onPressed: null, - icon: Icon(Icons.explore), - ), - IconButton( - tooltip: "Open settings", - onPressed: () => showDialog( - context: context, - builder: (_) => SettingsPage(), - ), - icon: const Icon(Icons.settings), - ), - ], - ), - ), - ), - ), - ), - ), - ); - } -} - -class const RoomsPane({super.key}) extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final selectedSpaceId = ref.watch( - KeyController.provider(KeyController.spaceKey) - .select((async) => async.requireValue), - ); - final selectedRoomId = ref.watch( - KeyController.provider(KeyController.roomKey) - .select((async) => async.requireValue), - ); - final selectedSpace = ref.watch( - SpacesController.provider.select((async) { - final spaces = async.requireValue; - return spaces.firstWhereOrNull( - (space) => space.id == selectedSpaceId, - ) ?? - spaces.first; - }), - ); - final isDm = selectedSpace.id == "dms"; - - return Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - leading: AvatarOrHash( - selectedSpace.room?.metadata?.avatar, - fallback: selectedSpace.icon == null - ? null - : Icon(selectedSpace.icon), - selectedSpace.title, - ), - title: Text(selectedSpace.title, overflow: .ellipsis), - backgroundColor: Colors.transparent, - actions: [ - RoomMenu(selectedSpace.room, children: selectedSpace.allChildRooms), - ], - ), - body: MaterialUiCompatibilityBridge( - child: Builder( - builder: (context) => old_mat.Theme( - data: old_mat.Theme.of(context).copyWith( - extensions: [ - NavigationRailM3ETheme( - itemExpandedHeight: 48, - iconLabelGap: 16, - ), - ], - ), - child: NavigationRailM3E( - expandedWidth: double.infinity, - scrollable: true, - background: Colors.transparent, - type: .alwaysExpand, - selectedIndex: max( - 0, - selectedSpace.allChildRooms.indexWhere( - (room) => room.metadata?.id == selectedRoomId, - ), - ), - sections: [ - .new( - header: selectedSpace.room == null - ? null - : const DividerWidget(Text("Rooms")), - destinations: roomDestinations( - selectedSpace.children, - isDm: isDm, - ), - ), - for (final subSpace in selectedSpace.subSpaces) - .new( - header: DividerWidget( - Row( - mainAxisSize: .min, - spacing: 8, - children: [ - if (subSpace.room.metadata?.avatar != null) - AvatarOrHash( - subSpace.room.metadata?.avatar, - subSpace.room.metadata?.name ?? "Unnamed Room", - dimension: 16, - ), - Flexible( - child: Text( - subSpace.room.metadata?.name ?? "Unnamed Space", - maxLines: 1, - overflow: .ellipsis, - ), - ), - ], - ), - ), - destinations: roomDestinations( - subSpace.children, - isDm: isDm, - ), - ), - ], - onDestinationSelected: (value) { - ref - .read( - KeyController.provider(KeyController.roomKey).notifier, - ) - .set(selectedSpace.allChildRooms[value].metadata?.id); - if (Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } - }, - ), - ), - ), - ), - ); - } -}