diff --git a/lib/helpers/extensions/scheme_to_theme.dart b/lib/helpers/extensions/scheme_to_theme.dart index ac50a09..f9a0521 100644 --- a/lib/helpers/extensions/scheme_to_theme.dart +++ b/lib/helpers/extensions/scheme_to_theme.dart @@ -9,6 +9,7 @@ extension SchemeToTheme on ColorScheme { ).textTheme; return .from(colorScheme: this).copyWith( cardTheme: .new(color: primaryContainer), + badgeTheme: .new(backgroundColor: primary, textColor: onPrimary), popupMenuTheme: .new( shape: RoundedRectangleBorder(borderRadius: .circular(16)), color: surfaceContainerHigh, diff --git a/lib/models/nav_page.dart b/lib/models/nav_page.dart index a731de2..083ead4 100644 --- a/lib/models/nav_page.dart +++ b/lib/models/nav_page.dart @@ -5,5 +5,6 @@ abstract class const NavPage({super.key}) extends Widget { String get title; IconData get icon; + /// Null hides the badge, and 0 shows a dot. int? badgeCount(WidgetRef ref) => null; } diff --git a/lib/widgets/count_badge.dart b/lib/widgets/count_badge.dart new file mode 100644 index 0000000..e0912f8 --- /dev/null +++ b/lib/widgets/count_badge.dart @@ -0,0 +1,16 @@ +import "package:material_ui/material_ui.dart"; + +/// Shows [count] in a badge on [child]: nothing if null, a dot if 0, and the +/// number otherwise. +class const CountBadge( + final int? count, { + required final Widget child, + super.key, +}) extends StatelessWidget { + @override + Widget build(BuildContext context) => switch (count) { + null => child, + 0 => Badge(child: child), + final count => Badge.count(count: count, child: child), + }; +} diff --git a/lib/widgets/room_appbar.dart b/lib/widgets/room_appbar.dart index 3b29129..6bbe2a3 100644 --- a/lib/widgets/room_appbar.dart +++ b/lib/widgets/room_appbar.dart @@ -3,8 +3,10 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:nexus/controllers/rooms.dart"; import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/avatar_or_hash.dart"; +import "package:nexus/widgets/count_badge.dart"; import "package:nexus/widgets/room_menu_button.dart"; import "package:nexus/widgets/room_summary_dialog.dart"; +import "package:nexus/widgets/sidebar.dart"; final class const RoomAppbar({ required final String? roomId, @@ -49,7 +51,14 @@ final class const RoomAppbar({ dimension: 24, fallback: Icon(Icons.numbers), ) - : DrawerButton(onPressed: onOpenDrawer), + : IconButton( + onPressed: onOpenDrawer, + tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, + icon: CountBadge( + Sidebar.totalBadgeCount(ref), + child: const DrawerButtonIcon(), + ), + ), scrolledUnderElevation: 0, title: room == null ? null diff --git a/lib/widgets/room_list.dart b/lib/widgets/room_list.dart index 7cf5e36..05c1219 100644 --- a/lib/widgets/room_list.dart +++ b/lib/widgets/room_list.dart @@ -13,6 +13,7 @@ import "package:nexus/widgets/pages/settings.dart"; import "package:nexus/widgets/join_dialog.dart"; import "package:nexus/widgets/room_menu_button.dart"; import "package:nexus/controllers/key.dart"; +import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/spaces.dart"; import "package:collection/collection.dart"; @@ -26,9 +27,26 @@ class const RoomList({super.key}) extends ConsumerWidget implements NavPage { @override IconData get icon => Icons.list; - // TODO: Show unread badge here and on sidebar hamburger @override - int? badgeCount(WidgetRef ref) => null; + 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) { diff --git a/lib/widgets/sidebar.dart b/lib/widgets/sidebar.dart index 4c80802..3ed8daa 100644 --- a/lib/widgets/sidebar.dart +++ b/lib/widgets/sidebar.dart @@ -1,23 +1,36 @@ +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/count_badge.dart"; import "package:nexus/widgets/invites_list.dart"; import "package:nexus/widgets/notifications_list.dart"; import "package:nexus/widgets/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( - (page) => switch (page) { - .rooms => const RoomList(), - .notifications => const NotificationsList(), - .invites => const InvitesList(), - }, - ); + final pages = SidebarPage.values.map(pageFor); return Drawer( width: 330, @@ -27,18 +40,17 @@ class const Sidebar({super.key}) extends ConsumerWidget { body: pages.elementAt(selected.index), bottomNavigationBar: NavigationBar( selectedIndex: selected.index, - destinations: pages.map((page) { - final badgeCount = page.badgeCount(ref); - - return NavigationDestination( - icon: Badge.count( - count: badgeCount ?? 0, - isLabelVisible: badgeCount != null, - child: Icon(page.icon), - ), - label: page.title, - ); - }).toList(), + 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]),