reorganize and rename widgets
This commit is contained in:
parent
05d25869ef
commit
d5f29dc344
7 changed files with 568 additions and 547 deletions
52
lib/widgets/sidebar/invite_card.dart
Normal file
52
lib/widgets/sidebar/invite_card.dart
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/controllers/invites.dart";
|
||||
import "package:nexus/helpers/extensions/get_localpart.dart";
|
||||
import "package:nexus/helpers/extensions/room_summary_display.dart";
|
||||
import "package:nexus/models/room_summary.dart";
|
||||
import "package:nexus/widgets/avatars/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/rooms/room_summary_sheet.dart";
|
||||
|
||||
class const InviteCard(final RoomSummary invite, {super.key})
|
||||
extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final responding = ref
|
||||
.watch(InvitesController.respondMutation(invite.roomId))
|
||||
.isPending;
|
||||
final name = invite.name ?? invite.canonicalAlias ?? invite.roomId;
|
||||
final inviterName =
|
||||
invite.inviterProfile?.displayName ?? invite.invitedBy?.localpart;
|
||||
|
||||
return Card(
|
||||
clipBehavior: .antiAlias,
|
||||
child: ListTile(
|
||||
onTap: responding
|
||||
? null
|
||||
: () => context.showAdaptiveSheet((_) => RoomSummarySheet(invite)),
|
||||
trailing: responding
|
||||
? const SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: null,
|
||||
leading: AvatarOrHash(
|
||||
invite.avatarUrl,
|
||||
name,
|
||||
dimension: 40,
|
||||
fallback: invite.isDirect ? null : Icon(invite.icon),
|
||||
),
|
||||
title: Text(name, maxLines: 1, overflow: .ellipsis),
|
||||
subtitle: Text(
|
||||
[
|
||||
if (inviterName != null)
|
||||
"${invite.isDirect ? "Direct message from" : "Invited by"} $inviterName",
|
||||
].join("\n"),
|
||||
maxLines: 2,
|
||||
overflow: .ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
90
lib/widgets/sidebar/invites_list.dart
Normal file
90
lib/widgets/sidebar/invites_list.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import "package:nexus/widgets/sidebar/invite_card.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/controllers/invites.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/models/nav_page.dart";
|
||||
|
||||
class const InvitesList({super.key}) extends ConsumerWidget implements NavPage {
|
||||
@override
|
||||
String get title => "Invites";
|
||||
|
||||
@override
|
||||
IconData get icon => Icons.inbox;
|
||||
|
||||
@override
|
||||
int? badgeCount(WidgetRef ref) => switch (ref.watch(
|
||||
InvitesController.provider.select((invites) => invites.length),
|
||||
)) {
|
||||
0 => null,
|
||||
final count => count,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final invites = ref.watch(InvitesController.provider);
|
||||
final decliningAll = ref
|
||||
.watch(InvitesController.declineAllMutation)
|
||||
.isPending;
|
||||
|
||||
Future<void> declineAll() async {
|
||||
final confirmed = await context.showAdaptiveSheet<bool>(
|
||||
(context) => SheetLayout(
|
||||
title: const Text("Decline All Invites"),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text("Decline All"),
|
||||
),
|
||||
],
|
||||
children: [
|
||||
Text(
|
||||
"Are you sure you want to decline all ${invites.length} invites?",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
|
||||
await InvitesController.declineAllMutation
|
||||
.run(
|
||||
ref,
|
||||
(_) => ref.read(InvitesController.provider.notifier).declineAll(),
|
||||
)
|
||||
.onError(showError);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
floatingActionButton: invites.isEmpty
|
||||
? null
|
||||
: FloatingActionButton.extended(
|
||||
onPressed: decliningAll ? null : declineAll,
|
||||
icon: decliningAll
|
||||
? const SizedBox.square(
|
||||
dimension: 24,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: const Icon(Icons.clear_all),
|
||||
label: const Text("Decline all"),
|
||||
),
|
||||
body: invites.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No invites",
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
)
|
||||
: ListView(
|
||||
// Leave room for the FAB under the last invite
|
||||
padding: const .only(left: 8, top: 8, right: 8, bottom: 88),
|
||||
children: invites.map(InviteCard.new).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
206
lib/widgets/sidebar/notifications_list.dart
Normal file
206
lib/widgets/sidebar/notifications_list.dart
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:m3e_buttons/m3e_buttons.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/notification_request.dart";
|
||||
import "package:nexus/controllers/notifications.dart";
|
||||
import "package:nexus/helpers/extensions/focus_room.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/models/nav_page.dart";
|
||||
import "package:nexus/widgets/common/error_dialog.dart";
|
||||
import "package:nexus/widgets/common/highlight_wrapper.dart";
|
||||
import "package:nexus/widgets/common/loading.dart";
|
||||
import "package:nexus/widgets/renderers/event.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
|
||||
class const NotificationsList({super.key})
|
||||
extends HookConsumerWidget
|
||||
implements NavPage {
|
||||
@override
|
||||
String get title => "Notifications";
|
||||
|
||||
@override
|
||||
IconData get icon => Icons.notifications;
|
||||
|
||||
@override
|
||||
Null badgeCount(_) => null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final options = <M3EToggleButtonGroupAction, UnreadType>{
|
||||
M3EToggleButtonGroupAction(
|
||||
checkedLabel: Text("Mentions"),
|
||||
icon: Icon(Icons.alternate_email),
|
||||
): .highlight,
|
||||
M3EToggleButtonGroupAction(
|
||||
checkedLabel: Text("All Notifications"),
|
||||
icon: Icon(Icons.notifications),
|
||||
): .notify,
|
||||
};
|
||||
final allNotificationsIndex = options.values.toList().indexOf(.notify);
|
||||
|
||||
final initialRequest = useMemoized(
|
||||
() => ref.read(NotificationRequestController.provider),
|
||||
);
|
||||
final unreadTypeIndex = useState(
|
||||
initialRequest == null ? 0 : allNotificationsIndex,
|
||||
);
|
||||
final highlightedId = useState(initialRequest);
|
||||
final listController = useRef(ListController());
|
||||
final scrollController = useScrollController();
|
||||
|
||||
final provider = NotificationsController.provider((
|
||||
options.values.toList()[unreadTypeIndex.value],
|
||||
null,
|
||||
));
|
||||
final notifications = ref.watch(provider);
|
||||
final notifier = ref.watch(provider.notifier);
|
||||
|
||||
void consumeRequest() => Future.microtask(
|
||||
ref.read(NotificationRequestController.provider.notifier).consume,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (initialRequest != null) consumeRequest();
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
ref.listen(NotificationRequestController.provider, (_, next) {
|
||||
if (next == null) return;
|
||||
unreadTypeIndex.value = allNotificationsIndex;
|
||||
highlightedId.value = next;
|
||||
consumeRequest();
|
||||
});
|
||||
|
||||
useEffect(() {
|
||||
final target = highlightedId.value;
|
||||
if (target == null) return null;
|
||||
|
||||
Timer? timer;
|
||||
var done = false;
|
||||
|
||||
void listener() => WidgetsBinding.instance.addPostFrameCallback((
|
||||
_,
|
||||
) async {
|
||||
if (done || !listController.value.isAttached) return;
|
||||
|
||||
final notifications = await ref.read(provider.future);
|
||||
|
||||
final index = notifications.indexWhere((tuple) => tuple.$1 == target);
|
||||
|
||||
if (done || index == -1) return;
|
||||
done = true;
|
||||
listController.value.removeListener(listener);
|
||||
|
||||
listController.value.animateToItem(
|
||||
index: index,
|
||||
scrollController: scrollController,
|
||||
alignment: 0.5,
|
||||
duration: (_) => Durations.extralong1,
|
||||
curve: (_) => Curves.easeInOut,
|
||||
);
|
||||
timer = Timer(Durations.extralong4, () {
|
||||
highlightedId.value = null;
|
||||
});
|
||||
});
|
||||
|
||||
listController.value.addListener(listener);
|
||||
// The list may already be laid out, in which case no change is notified
|
||||
listener();
|
||||
|
||||
return () {
|
||||
done = true;
|
||||
listController.value.removeListener(listener);
|
||||
timer?.cancel();
|
||||
};
|
||||
}, [highlightedId.value]);
|
||||
|
||||
useEffect(() {
|
||||
Future<void> listener() async {
|
||||
if (!scrollController.hasClients || notifications.isLoading) return;
|
||||
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent) {
|
||||
await notifier.loadOlder();
|
||||
}
|
||||
}
|
||||
|
||||
scrollController.addListener(listener);
|
||||
return () => scrollController.removeListener(listener);
|
||||
}, [scrollController, notifications]);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
if (notifications is AsyncLoading && notifications.value != null)
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
Expanded(
|
||||
child: switch (notifications) {
|
||||
AsyncData(:final value) || AsyncLoading(:final value?) =>
|
||||
value.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No notifications yet",
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
)
|
||||
: SuperListView.builder(
|
||||
listController: listController.value,
|
||||
controller: scrollController,
|
||||
itemCount: value.length,
|
||||
padding: const .symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 12,
|
||||
),
|
||||
reverse: true,
|
||||
itemBuilder: (context, index) {
|
||||
final (rowId, roomId) = value[index];
|
||||
final isHighlighted = rowId == highlightedId.value;
|
||||
|
||||
return Padding(
|
||||
padding: .only(top: 8),
|
||||
child: HighlightWrapper(
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final navigator = Navigator.of(context);
|
||||
if (navigator.canPop()) navigator.pop();
|
||||
|
||||
await ref.focusRoom(roomId, rowId);
|
||||
},
|
||||
child: IgnorePointer(
|
||||
child: EventRenderer(rowId, roomId: roomId),
|
||||
),
|
||||
),
|
||||
isHighlighted: isHighlighted,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
AsyncLoading() => const Loading(),
|
||||
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
||||
error,
|
||||
stackTrace,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: .topRight,
|
||||
child: Padding(
|
||||
padding: .all(16),
|
||||
child: M3EToggleButtonGroup(
|
||||
selectedIndex: unreadTypeIndex.value,
|
||||
onSelectedIndexChanged: (index) =>
|
||||
unreadTypeIndex.value = index ?? unreadTypeIndex.value,
|
||||
actions: options.keys.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/widgets/sidebar/room_list.dart
Normal file
59
lib/widgets/sidebar/room_list.dart
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import "package:nexus/widgets/sidebar/spaces_rail.dart";
|
||||
import "package:nexus/widgets/sidebar/rooms_pane.dart";
|
||||
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/models/nav_page.dart";
|
||||
import "package:nexus/widgets/common/error_dialog.dart";
|
||||
import "package:nexus/controllers/rooms.dart";
|
||||
import "package:nexus/controllers/spaces.dart";
|
||||
|
||||
class const RoomList({super.key}) extends ConsumerWidget implements NavPage {
|
||||
@override
|
||||
String get title => "Rooms";
|
||||
|
||||
@override
|
||||
IconData get icon => Icons.list;
|
||||
|
||||
@override
|
||||
int? badgeCount(WidgetRef ref) {
|
||||
final (notifications, hasUnread) = ref.watch(
|
||||
RoomsController.provider.select(
|
||||
(rooms) => rooms.values.fold(
|
||||
(0, false),
|
||||
(total, room) => (
|
||||
total.$1 + (room.metadata?.unreadNotifications ?? 0),
|
||||
total.$2 || (room.metadata?.unreadMessages ?? 0) > 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return notifications > 0
|
||||
? notifications
|
||||
: hasUnread
|
||||
? 0
|
||||
: null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final spacesAsync = ref.watch(SpacesController.provider);
|
||||
if (spacesAsync.value == null) {
|
||||
return switch (spacesAsync) {
|
||||
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
||||
error,
|
||||
stackTrace,
|
||||
),
|
||||
_ => const Center(child: CircularProgressIndicator()),
|
||||
};
|
||||
}
|
||||
|
||||
return const Row(
|
||||
children: [
|
||||
SpacesRail(),
|
||||
Expanded(child: RoomsPane()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
143
lib/widgets/sidebar/rooms_pane.dart
Normal file
143
lib/widgets/sidebar/rooms_pane.dart
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import "dart:math";
|
||||
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
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:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/helpers/extensions/get_all_child_rooms.dart";
|
||||
import "package:nexus/helpers/extensions/room_destinations.dart";
|
||||
import "package:nexus/widgets/common/appbar.dart";
|
||||
import "package:nexus/widgets/rooms/room_title.dart";
|
||||
import "package:nexus/widgets/avatars/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/common/divider_widget.dart";
|
||||
import "package:nexus/widgets/common/m3e_rail_theme.dart";
|
||||
import "package:nexus/widgets/rooms/room_summary_sheet.dart";
|
||||
import "package:nexus/widgets/rooms/room_menu_button.dart";
|
||||
import "package:nexus/widgets/rooms/space_hierarchy_sheet.dart";
|
||||
import "package:nexus/controllers/key.dart";
|
||||
import "package:nexus/controllers/selected_room.dart";
|
||||
import "package:nexus/controllers/selected_space.dart";
|
||||
|
||||
class const RoomsPane({super.key}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selectedRoomId = ref.watch(SelectedRoomController.provider);
|
||||
final selectedSpace = ref.watch(SelectedSpaceController.provider)!;
|
||||
final isDm = selectedSpace.id == "dms";
|
||||
final spaceMetadata = selectedSpace.room?.metadata;
|
||||
// In the order the rail shows them, with the space's own timeline first
|
||||
final destinationIds = IList<String?>([?spaceMetadata?.id])
|
||||
.addAll(selectedSpace.allChildRooms.map((room) => room.metadata?.id));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: Appbar(
|
||||
// Home and DMs aren't real spaces, so they don't have a summary.
|
||||
onTap: spaceMetadata == null
|
||||
? null
|
||||
: () => context.showAdaptiveSheet(
|
||||
(_) => RoomSummarySheet(.fromMetadata(spaceMetadata)),
|
||||
),
|
||||
windowControls: false,
|
||||
leading: AvatarOrHash(
|
||||
spaceMetadata?.avatar,
|
||||
fallback: selectedSpace.icon == null
|
||||
? null
|
||||
: Icon(selectedSpace.icon),
|
||||
selectedSpace.title,
|
||||
),
|
||||
title: RoomTitle(selectedSpace.title, topic: spaceMetadata?.topic),
|
||||
backgroundColor: Colors.transparent,
|
||||
actions: .new([
|
||||
RoomMenuButton(
|
||||
selectedSpace.room,
|
||||
children: selectedSpace.allChildRooms,
|
||||
),
|
||||
]),
|
||||
),
|
||||
body: M3ERailTheme(
|
||||
extensions: [
|
||||
NavigationRailM3ETheme(
|
||||
itemExpandedHeight: 48,
|
||||
iconLabelGap: 16,
|
||||
sectionHeaderSpacingTop: 0,
|
||||
),
|
||||
],
|
||||
builder: (context) => Padding(
|
||||
padding: const .only(top: 4),
|
||||
child: NavigationRailM3E(
|
||||
fab: selectedSpace.room == null
|
||||
? null
|
||||
: .new(
|
||||
icon: const Icon(Icons.explore),
|
||||
label: "Browse space",
|
||||
tooltip: "Browse and join rooms in this space",
|
||||
onPressed: () => context.showAdaptiveSheet(
|
||||
(_) => SpaceHierarchySheet(selectedSpace.id),
|
||||
),
|
||||
),
|
||||
expandedWidth: double.infinity,
|
||||
scrollable: true,
|
||||
background: Colors.transparent,
|
||||
type: .alwaysExpand,
|
||||
selectedIndex: max(0, destinationIds.indexOf(selectedRoomId)),
|
||||
sections: [
|
||||
if (selectedSpace.room case final room?
|
||||
when spaceMetadata != null)
|
||||
.new(
|
||||
destinations: [
|
||||
room.toDestination(
|
||||
isDm: isDm,
|
||||
label: "Space timeline",
|
||||
icon: const Icon(Icons.forum),
|
||||
),
|
||||
],
|
||||
),
|
||||
.new(
|
||||
header: selectedSpace.room == null
|
||||
? null
|
||||
: const DividerWidget(Text("Rooms")),
|
||||
destinations: selectedSpace.children.toDestinations(isDm: isDm),
|
||||
),
|
||||
...selectedSpace.subSpaces.map(
|
||||
(subSpace) => NavigationRailM3ESection(
|
||||
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: subSpace.children.toDestinations(isDm: isDm),
|
||||
),
|
||||
),
|
||||
],
|
||||
onDestinationSelected: (value) {
|
||||
ref
|
||||
.read(KeyController.provider(KeyController.roomKey).notifier)
|
||||
.set(destinationIds[value]);
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/widgets/sidebar/sidebar.dart
Normal file
61
lib/widgets/sidebar/sidebar.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:nexus/controllers/sidebar_page.dart";
|
||||
import "package:nexus/models/nav_page.dart";
|
||||
import "package:nexus/models/sidebar_page.dart";
|
||||
import "package:nexus/widgets/common/count_badge.dart";
|
||||
import "package:nexus/widgets/sidebar/invites_list.dart";
|
||||
import "package:nexus/widgets/sidebar/notifications_list.dart";
|
||||
import "package:nexus/widgets/sidebar/room_list.dart";
|
||||
|
||||
class const Sidebar({super.key}) extends ConsumerWidget {
|
||||
static NavPage pageFor(SidebarPage page) => switch (page) {
|
||||
.rooms => const RoomList(),
|
||||
.notifications => const NotificationsList(),
|
||||
.invites => const InvitesList(),
|
||||
};
|
||||
|
||||
/// The badges of every tab added together, e.g. for the drawer button.
|
||||
///
|
||||
/// Like [NavPage.badgeCount], null means no badges and 0 means only dots.
|
||||
static int? totalBadgeCount(WidgetRef ref) {
|
||||
final counts = SidebarPage.values
|
||||
.map((page) => pageFor(page).badgeCount(ref))
|
||||
.nonNulls;
|
||||
|
||||
return counts.isEmpty ? null : counts.sum;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final selected = ref.watch(SidebarPageController.provider);
|
||||
final pages = SidebarPage.values.map(pageFor);
|
||||
|
||||
return Drawer(
|
||||
width: 330,
|
||||
shape: Border(),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: pages.elementAt(selected.index),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: selected.index,
|
||||
destinations: pages
|
||||
.map(
|
||||
(page) => NavigationDestination(
|
||||
icon: CountBadge(
|
||||
page.badgeCount(ref),
|
||||
child: Icon(page.icon),
|
||||
),
|
||||
label: page.title,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onDestinationSelected: (index) => ref
|
||||
.read(SidebarPageController.provider.notifier)
|
||||
.set(SidebarPage.values[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
142
lib/widgets/sidebar/spaces_rail.dart
Normal file
142
lib/widgets/sidebar/spaces_rail.dart
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
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:material_adaptive_sheets/material_adaptive_sheets.dart";
|
||||
import "package:nexus/helpers/extensions/get_all_child_rooms.dart";
|
||||
import "package:nexus/widgets/common/account_switcher.dart";
|
||||
import "package:nexus/widgets/avatars/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/rooms/create_room_sheet.dart";
|
||||
import "package:nexus/widgets/users/start_dm_sheet.dart";
|
||||
import "package:nexus/widgets/pages/explore.dart";
|
||||
import "package:nexus/widgets/pages/settings.dart";
|
||||
import "package:nexus/widgets/rooms/join_sheet.dart";
|
||||
import "package:nexus/widgets/common/m3e_rail_theme.dart";
|
||||
import "package:nexus/controllers/key.dart";
|
||||
import "package:nexus/controllers/spaces.dart";
|
||||
|
||||
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 M3ERailTheme(
|
||||
extensions: [
|
||||
NavigationRailM3ETheme(itemCollapsedHeight: 48, itemVerticalGap: 0),
|
||||
],
|
||||
builder: (context) => 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: [
|
||||
AdaptiveMenuButton(
|
||||
tooltip: "Add a room",
|
||||
itemBuilder: (_) => [
|
||||
PopupMenuItem(
|
||||
onTap: () =>
|
||||
context.showAdaptiveSheet((_) => JoinSheet(ref)),
|
||||
child: const ListTile(
|
||||
title: Text("Join an existing room (or space)"),
|
||||
leading: Icon(Icons.numbers),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => context.showAdaptiveSheet(
|
||||
(_) => const CreateRoomSheet(),
|
||||
),
|
||||
child: const ListTile(
|
||||
title: Text("Create a new room (or space)"),
|
||||
leading: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
onTap: () => context.showAdaptiveSheet(
|
||||
(_) => const StartDmSheet(),
|
||||
),
|
||||
child: const ListTile(
|
||||
title: Text("Start a direct message"),
|
||||
leading: Icon(Icons.person_add),
|
||||
),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
const AccountSwitcher(),
|
||||
IconButton(
|
||||
tooltip: "Explore other rooms",
|
||||
onPressed: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const ExplorePage()),
|
||||
),
|
||||
icon: const Icon(Icons.explore),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: "Open settings",
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => SettingsPage(),
|
||||
),
|
||||
icon: const Icon(Icons.settings),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue