show badges totaled in more areas

This commit is contained in:
Henry Hiles 2026-10-15 00:42:56 -04:00
commit 66a437094a
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
6 changed files with 79 additions and 22 deletions

View file

@ -9,6 +9,7 @@ extension SchemeToTheme on ColorScheme {
).textTheme; ).textTheme;
return .from(colorScheme: this).copyWith( return .from(colorScheme: this).copyWith(
cardTheme: .new(color: primaryContainer), cardTheme: .new(color: primaryContainer),
badgeTheme: .new(backgroundColor: primary, textColor: onPrimary),
popupMenuTheme: .new( popupMenuTheme: .new(
shape: RoundedRectangleBorder(borderRadius: .circular(16)), shape: RoundedRectangleBorder(borderRadius: .circular(16)),
color: surfaceContainerHigh, color: surfaceContainerHigh,

View file

@ -5,5 +5,6 @@ abstract class const NavPage({super.key}) extends Widget {
String get title; String get title;
IconData get icon; IconData get icon;
/// Null hides the badge, and 0 shows a dot.
int? badgeCount(WidgetRef ref) => null; int? badgeCount(WidgetRef ref) => null;
} }

View file

@ -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),
};
}

View file

@ -3,8 +3,10 @@ import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:nexus/controllers/rooms.dart"; import "package:nexus/controllers/rooms.dart";
import "package:nexus/widgets/appbar.dart"; import "package:nexus/widgets/appbar.dart";
import "package:nexus/widgets/avatar_or_hash.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_menu_button.dart";
import "package:nexus/widgets/room_summary_dialog.dart"; import "package:nexus/widgets/room_summary_dialog.dart";
import "package:nexus/widgets/sidebar.dart";
final class const RoomAppbar({ final class const RoomAppbar({
required final String? roomId, required final String? roomId,
@ -49,7 +51,14 @@ final class const RoomAppbar({
dimension: 24, dimension: 24,
fallback: Icon(Icons.numbers), 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, scrolledUnderElevation: 0,
title: room == null title: room == null
? null ? null

View file

@ -13,6 +13,7 @@ import "package:nexus/widgets/pages/settings.dart";
import "package:nexus/widgets/join_dialog.dart"; import "package:nexus/widgets/join_dialog.dart";
import "package:nexus/widgets/room_menu_button.dart"; import "package:nexus/widgets/room_menu_button.dart";
import "package:nexus/controllers/key.dart"; import "package:nexus/controllers/key.dart";
import "package:nexus/controllers/rooms.dart";
import "package:nexus/controllers/spaces.dart"; import "package:nexus/controllers/spaces.dart";
import "package:collection/collection.dart"; import "package:collection/collection.dart";
@ -26,9 +27,26 @@ class const RoomList({super.key}) extends ConsumerWidget implements NavPage {
@override @override
IconData get icon => Icons.list; IconData get icon => Icons.list;
// TODO: Show unread badge here and on sidebar hamburger
@override @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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {

View file

@ -1,23 +1,36 @@
import "package:collection/collection.dart";
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:nexus/controllers/sidebar_page.dart"; import "package:nexus/controllers/sidebar_page.dart";
import "package:nexus/models/nav_page.dart"; import "package:nexus/models/nav_page.dart";
import "package:nexus/models/sidebar_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/invites_list.dart";
import "package:nexus/widgets/notifications_list.dart"; import "package:nexus/widgets/notifications_list.dart";
import "package:nexus/widgets/room_list.dart"; import "package:nexus/widgets/room_list.dart";
class const Sidebar({super.key}) extends ConsumerWidget { class const Sidebar({super.key}) extends ConsumerWidget {
@override static NavPage pageFor(SidebarPage page) => switch (page) {
Widget build(BuildContext context, WidgetRef ref) {
final selected = ref.watch(SidebarPageController.provider);
final pages = SidebarPage.values.map<NavPage>(
(page) => switch (page) {
.rooms => const RoomList(), .rooms => const RoomList(),
.notifications => const NotificationsList(), .notifications => const NotificationsList(),
.invites => const InvitesList(), .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( return Drawer(
width: 330, width: 330,
@ -27,18 +40,17 @@ class const Sidebar({super.key}) extends ConsumerWidget {
body: pages.elementAt(selected.index), body: pages.elementAt(selected.index),
bottomNavigationBar: NavigationBar( bottomNavigationBar: NavigationBar(
selectedIndex: selected.index, selectedIndex: selected.index,
destinations: pages.map((page) { destinations: pages
final badgeCount = page.badgeCount(ref); .map(
(page) => NavigationDestination(
return NavigationDestination( icon: CountBadge(
icon: Badge.count( page.badgeCount(ref),
count: badgeCount ?? 0,
isLabelVisible: badgeCount != null,
child: Icon(page.icon), child: Icon(page.icon),
), ),
label: page.title, label: page.title,
); ),
}).toList(), )
.toList(),
onDestinationSelected: (index) => ref onDestinationSelected: (index) => ref
.read(SidebarPageController.provider.notifier) .read(SidebarPageController.provider.notifier)
.set(SidebarPage.values[index]), .set(SidebarPage.values[index]),