Compare commits

..
3 changed files with 258 additions and 255 deletions

View file

@ -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<Room> get allChildRooms =>
children.addAll(subSpaces.expand((s) => s.children));
}

View file

@ -1,6 +1,5 @@
import "dart:ui"; import "dart:ui";
import "package:flutter/foundation.dart";
import "package:flutter/widgets.dart"; import "package:flutter/widgets.dart";
import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client.dart";
@ -10,18 +9,19 @@ class MxcImage(final WidgetRef ref, final DownloadMediaRequest request)
extends ImageProvider<MxcImage> { extends ImageProvider<MxcImage> {
@override @override
Future<MxcImage> obtainKey(ImageConfiguration configuration) => Future<MxcImage> obtainKey(ImageConfiguration configuration) =>
SynchronousFuture(this); Future.value(this);
@override @override
ImageStreamCompleter loadImage(MxcImage key, ImageDecoderCallback decode) => ImageStreamCompleter loadImage(MxcImage key, ImageDecoderCallback decode) =>
MultiFrameImageStreamCompleter(codec: _loadAsync(decode), scale: 1); MultiFrameImageStreamCompleter(codec: _loadAsync(decode), scale: 1.0);
Future<Codec> _loadAsync(ImageDecoderCallback decode) async { Future<Codec> _loadAsync(ImageDecoderCallback decode) async {
final file = await ref final file = await ref
.read(ClientController.provider.notifier) .read(ClientController.provider.notifier)
.downloadMedia(request); .downloadMedia(request);
final buffer = await ImmutableBuffer.fromFilePath(file.path);
return decode(await ImmutableBuffer.fromFilePath(file.path)); return decode(buffer);
} }
@override @override

View file

@ -1,9 +1,6 @@
import "dart:math";
import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
import "package:navigation_rail_m3e/navigation_rail_m3e.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/models/nav_page.dart";
import "package:nexus/widgets/avatar_or_hash.dart"; import "package:nexus/widgets/avatar_or_hash.dart";
import "package:nexus/widgets/divider_widget.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). // Needed for navigation_rail_m3e (#65).
import "package:flutter/material.dart" as old_mat; import "package:flutter/material.dart" as old_mat;
List<NavigationRailM3EDestination> roomDestinations( class const RoomList({super.key})
IList<Room> rooms, { extends HookConsumerWidget
required bool isDm, implements NavPage {
}) => [
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 {
@override @override
String get title => "Rooms"; String get title => "Rooms";
@ -52,8 +32,21 @@ class const RoomList({super.key}) extends ConsumerWidget implements NavPage {
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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); final spacesAsync = ref.watch(SpacesController.provider);
if (spacesAsync.value == null) { final spaces = spacesAsync.value;
if (spaces == null) {
return switch (spacesAsync) { return switch (spacesAsync) {
AsyncError(:final error, :final stackTrace) => ErrorDialog( AsyncError(:final error, :final stackTrace) => ErrorDialog(
error, error,
@ -63,27 +56,48 @@ 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<NavigationRailM3EDestination> roomsToDestinations(IList<Room> 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: [ children: [
SpacesRail(), MaterialUiCompatibilityBridge(
Expanded(child: RoomsPane()),
],
);
}
}
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( child: Builder(
builder: (context) => old_mat.Theme( builder: (context) => old_mat.Theme(
data: old_mat.Theme.of(context).copyWith( data: old_mat.Theme.of(context).copyWith(
@ -96,56 +110,69 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
), ),
child: Container( child: Container(
color: NavigationRailTokensAdapter(context).containerColor, color: NavigationRailTokensAdapter(context).containerColor,
padding: .only(top: 16), padding: EdgeInsets.only(top: 16),
child: NavigationRailM3E( child: NavigationRailM3E(
type: .alwaysCollapse, type: .alwaysCollapse,
labelBehavior: .alwaysHide, labelBehavior: .alwaysHide,
scrollable: true, scrollable: true,
onDestinationSelected: (value) { onDestinationSelected: (value) {
ref selectedSpaceIdNotifier.set(spaces[value].id);
.read( selectedRoomIdNotifier.set(
KeyController.provider(KeyController.spaceKey).notifier, spaces[value].children.firstOrNull?.metadata?.id,
) );
.set(spaces[value].id);
ref
.read(
KeyController.provider(KeyController.roomKey).notifier,
)
.set(spaces[value].children.firstOrNull?.metadata?.id);
}, },
sections: [ sections: [
.new( .new(
destinations: spaces.map((space) { destinations: spaces
final notifications = space.allChildRooms.fold( .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,
(sum, room) =>
sum + (room.metadata?.unreadNotifications ?? 0),
);
return NavigationRailM3EDestination(
badgeCount: notifications > 0
? notifications
: space.allChildRooms.any(
(room) => room.metadata?.unreadMessages != 0,
) )
? 0 ? 0
: null, : null,
int badgeCount => badgeCount,
},
short: true, short: true,
icon: AvatarOrHash( icon: AvatarOrHash(
dimension: 28, dimension: 28,
space.room?.metadata?.avatar, space.room?.metadata?.avatar,
fallback: space.icon == null ? null : Icon(space.icon), fallback: space.icon == null
? null
: Icon(space.icon),
space.title, space.title,
), ),
label: space.title, label: space.title,
); ),
}).toList(), )
.toList(),
), ),
], ],
selectedIndex: max( selectedIndex: selectedIndex,
0,
spaces.indexWhere((space) => space.id == selectedSpaceId),
),
trailingAtBottom: true, trailingAtBottom: true,
trailing: Padding( trailing: Padding(
padding: .only(top: 8), padding: .only(top: 8),
@ -158,12 +185,12 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
context: context, context: context,
builder: (_) => JoinDialog(ref), builder: (_) => JoinDialog(ref),
), ),
child: const ListTile( child: ListTile(
title: Text("Join an existing room (or space)"), title: Text("Join an existing room (or space)"),
leading: Icon(Icons.numbers), leading: Icon(Icons.numbers),
), ),
), ),
const PopupMenuItem( PopupMenuItem(
onTap: null, onTap: null,
child: ListTile( child: ListTile(
title: Text("Create a new room"), title: Text("Create a new room"),
@ -171,9 +198,9 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
), ),
), ),
], ],
icon: const Icon(Icons.add), icon: Icon(Icons.add),
), ),
const IconButton( IconButton(
tooltip: "Explore other rooms", tooltip: "Explore other rooms",
onPressed: null, onPressed: null,
icon: Icon(Icons.explore), icon: Icon(Icons.explore),
@ -184,7 +211,7 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
context: context, context: context,
builder: (_) => SettingsPage(), builder: (_) => SettingsPage(),
), ),
icon: const Icon(Icons.settings), icon: Icon(Icons.settings),
), ),
], ],
), ),
@ -193,33 +220,10 @@ class const SpacesRail({super.key}) extends ConsumerWidget {
), ),
), ),
), ),
); ),
}
}
class const RoomsPane({super.key}) extends ConsumerWidget { Expanded(
@override child: Scaffold(
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, backgroundColor: Colors.transparent,
appBar: AppBar( appBar: AppBar(
leading: AvatarOrHash( leading: AvatarOrHash(
@ -227,12 +231,20 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
fallback: selectedSpace.icon == null fallback: selectedSpace.icon == null
? null ? null
: Icon(selectedSpace.icon), : Icon(selectedSpace.icon),
selectedSpace.title, selectedSpace.title,
), ),
title: Text(selectedSpace.title, overflow: .ellipsis), title: Text(selectedSpace.title, overflow: .ellipsis),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
actions: [ actions: [
RoomMenu(selectedSpace.room, children: selectedSpace.allChildRooms), RoomMenu(
selectedSpace.room,
children: selectedSpace.children.addAll(
selectedSpace.subSpaces
.map((element) => element.children)
.flattened,
),
),
], ],
), ),
body: MaterialUiCompatibilityBridge( body: MaterialUiCompatibilityBridge(
@ -251,57 +263,53 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
scrollable: true, scrollable: true,
background: Colors.transparent, background: Colors.transparent,
type: .alwaysExpand, type: .alwaysExpand,
selectedIndex: max( selectedIndex: selectedRoomIndex ?? 0,
0,
selectedSpace.allChildRooms.indexWhere(
(room) => room.metadata?.id == selectedRoomId,
),
),
sections: [ sections: [
.new( .new(
header: selectedSpace.room == null header: selectedSpace.room == null
? null ? null
: const DividerWidget(Text("Rooms")), : DividerWidget(Text("Rooms")),
destinations: roomDestinations( destinations: roomsToDestinations(
selectedSpace.children, selectedSpace.children,
isDm: isDm,
), ),
), ),
for (final subSpace in selectedSpace.subSpaces) for (final subSpace in selectedSpace.subSpaces)
.new( .new(
header: DividerWidget( header: DividerWidget(
Row( Row(
mainAxisSize: .min, mainAxisSize: MainAxisSize.min,
spacing: 8, spacing: 8,
children: [ children: [
if (subSpace.room.metadata?.avatar != null) if (subSpace.room.metadata?.avatar != null)
AvatarOrHash( AvatarOrHash(
subSpace.room.metadata?.avatar, subSpace.room.metadata?.avatar,
subSpace.room.metadata?.name ?? "Unnamed Room", subSpace.room.metadata?.name ??
"Unnamed Room",
dimension: 16, dimension: 16,
), ),
Flexible( Flexible(
child: Text( child: Text(
subSpace.room.metadata?.name ?? "Unnamed Space", subSpace.room.metadata?.name ??
"Unnamed Space",
maxLines: 1, maxLines: 1,
overflow: .ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
], ],
), ),
), ),
destinations: roomDestinations( destinations: roomsToDestinations(subSpace.children),
subSpace.children,
isDm: isDm,
),
), ),
], ],
onDestinationSelected: (value) { onDestinationSelected: (value) {
ref final children = selectedSpace.children.addAll(
.read( selectedSpace.subSpaces
KeyController.provider(KeyController.roomKey).notifier, .map((element) => element.children)
) .flattened,
.set(selectedSpace.allChildRooms[value].metadata?.id); );
selectedRoomIdNotifier.set(
children[value].metadata?.id, //
);
if (Navigator.of(context).canPop()) { if (Navigator.of(context).canPop()) {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
@ -310,6 +318,9 @@ class const RoomsPane({super.key}) extends ConsumerWidget {
), ),
), ),
), ),
),
),
],
); );
} }
} }