diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 75c7fd90e2..436406a026 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -11,6 +11,7 @@ import 'about_zulip.dart'; import 'inbox.dart'; import 'login.dart'; import 'message_list.dart'; +import 'navigation_bar.dart'; import 'page.dart'; import 'recent_dm_conversations.dart'; import 'store.dart'; @@ -177,7 +178,7 @@ class ChooseAccountPage extends StatelessWidget { title: title, subtitle: subtitle, onTap: () => Navigator.push(context, - HomePage.buildRoute(accountId: accountId)))); + InboxPage.buildRoute(accountId: accountId)))); } @override @@ -255,6 +256,7 @@ class HomePage extends StatelessWidget { return Scaffold( appBar: AppBar(title: const Text("Home")), + bottomNavigationBar: ZulipNavigationBar(selectedPage: HomePage,), body: Center( child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ DefaultTextStyle.merge( diff --git a/lib/widgets/inbox.dart b/lib/widgets/inbox.dart index 8aa5172efc..a3b80a94e1 100644 --- a/lib/widgets/inbox.dart +++ b/lib/widgets/inbox.dart @@ -6,13 +6,14 @@ import '../model/recent_dm_conversations.dart'; import '../model/unreads.dart'; import 'icons.dart'; import 'message_list.dart'; +import 'navigation_bar.dart'; import 'page.dart'; import 'sticky_header.dart'; import 'store.dart'; import 'text.dart'; import 'unread_count_badge.dart'; -class InboxPage extends StatefulWidget { +class InboxPage extends StatelessWidget { const InboxPage({super.key}); static Route buildRoute({int? accountId, BuildContext? context}) { @@ -21,10 +22,44 @@ class InboxPage extends StatefulWidget { } @override - State createState() => _InboxPageState(); + Widget build(BuildContext context) { + return DefaultTabController( + initialIndex: 0, + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text('Inbox'), + bottom: const TabBar( + tabs: [ + Tab( + text: 'Streams + Topics', + ), + Tab( + text: 'All Messages', + ), + ], + ), + ), + bottomNavigationBar: ZulipNavigationBar(selectedPage: InboxPage), + body: const SafeArea(child: TabBarView( + children: [ + Inbox(), + MessageList(narrow: AllMessagesNarrow()), + ], + )) , + ), + ); + } } -class _InboxPageState extends State with PerAccountStoreAwareStateMixin { +class Inbox extends StatefulWidget { + const Inbox({super.key}); + + @override + State createState() => _InboxState(); +} + +class _InboxState extends State with PerAccountStoreAwareStateMixin { Unreads? unreadsModel; RecentDmConversationsView? recentDmConversationsModel; @@ -158,27 +193,22 @@ class _InboxPageState extends State with PerAccountStoreAwareStateMix sections.add(_StreamSectionData(streamId, countInStream, streamHasMention, topicItems)); } - return Scaffold( - appBar: AppBar(title: const Text('Inbox')), - body: SafeArea( - // Don't pad the bottom here; we want the list content to do that. - bottom: false, - child: StickyHeaderListView.builder( - itemCount: sections.length, - itemBuilder: (context, index) { - final section = sections[index]; - switch (section) { - case _AllDmsSectionData(): - return _AllDmsSection( - data: section, - collapsed: allDmsCollapsed, - pageState: this, - ); - case _StreamSectionData(:var streamId): - final collapsed = collapsedStreamIds.contains(streamId); - return _StreamSection(data: section, collapsed: collapsed, pageState: this); - } - }))); + return StickyHeaderListView.builder( + itemCount: sections.length, + itemBuilder: (context, index) { + final section = sections[index]; + switch (section) { + case _AllDmsSectionData(): + return _AllDmsSection( + data: section, + collapsed: allDmsCollapsed, + pageState: this, + ); + case _StreamSectionData(:var streamId): + final collapsed = collapsedStreamIds.contains(streamId); + return _StreamSection(data: section, collapsed: collapsed, pageState: this); + } + }); } } @@ -205,7 +235,7 @@ class _StreamSectionData extends _InboxSectionData { abstract class _HeaderItem extends StatelessWidget { final bool collapsed; - final _InboxPageState pageState; + final _InboxState pageState; final int count; final bool hasMention; @@ -295,7 +325,7 @@ class _AllDmsSection extends StatelessWidget { final _AllDmsSectionData data; final bool collapsed; - final _InboxPageState pageState; + final _InboxState pageState; @override Widget build(BuildContext context) { @@ -416,7 +446,7 @@ class _StreamSection extends StatelessWidget { final _StreamSectionData data; final bool collapsed; - final _InboxPageState pageState; + final _InboxState pageState; @override Widget build(BuildContext context) { diff --git a/lib/widgets/navigation_bar.dart b/lib/widgets/navigation_bar.dart new file mode 100644 index 0000000000..a147765d52 --- /dev/null +++ b/lib/widgets/navigation_bar.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +import 'app.dart'; +import 'inbox.dart'; +import 'recent_dm_conversations.dart'; +import 'store.dart'; +import 'subscription_list.dart'; + +class ZulipNavigationBar extends StatelessWidget { + final int testStreamId = 7; + final Type selectedPage; + final Map pageToIndex = { + InboxPage: 0, + SubscriptionListPage: 1, + RecentDmConversationsPage: 2, + HomePage: 3, + }; + + ZulipNavigationBar({super.key, required this.selectedPage}); + + @override + Widget build(BuildContext context) { + final accountId = PerAccountStoreWidget.accountIdOf(context); + final selectedIndex = pageToIndex[selectedPage] ?? 0; + return NavigationBar( + selectedIndex: selectedIndex, + destinations: const [ + NavigationDestination( + selectedIcon: Icon(Icons.inbox), + icon: Icon(Icons.inbox_outlined), + label: 'Inbox'), + NavigationDestination( + selectedIcon: Icon(Icons.tag), + icon: Icon(Icons.tag_outlined), + label: 'Streams'), + NavigationDestination( + selectedIcon: Icon(Icons.group), + icon: Icon(Icons.group_outlined), + label: 'Direct Messages'), + // TODO enable this when the profile page is available + // NavigationDestination( + // selectedIcon: Icon(Icons.account_circle), + // icon: Icon(Icons.account_circle_outlined), + // label: 'Profile'), + NavigationDestination( + selectedIcon: Icon(Icons.bug_report_outlined), + icon: Icon(Icons.bug_report_outlined), + label: 'Test Page'), + ], + onDestinationSelected: (int index) { + if (selectedIndex == index) { + return; + } + switch (index) { + case 0: + Navigator.pushReplacement(context, InboxPage.buildRoute(context: context)); + break; + case 1: + Navigator.pushReplacement(context, SubscriptionListPage.buildRoute(context: context)); + break; + case 2: + Navigator.pushReplacement(context, RecentDmConversationsPage.buildRoute(context: context)); + break; + case 3: + Navigator.pushReplacement(context, + HomePage.buildRoute(accountId: accountId)); + break; + } + }, + ); + } +} \ No newline at end of file diff --git a/lib/widgets/recent_dm_conversations.dart b/lib/widgets/recent_dm_conversations.dart index 27bf2a7560..3a5e164dcf 100644 --- a/lib/widgets/recent_dm_conversations.dart +++ b/lib/widgets/recent_dm_conversations.dart @@ -7,6 +7,7 @@ import '../model/unreads.dart'; import 'content.dart'; import 'icons.dart'; import 'message_list.dart'; +import 'navigation_bar.dart'; import 'page.dart'; import 'store.dart'; import 'unread_count_badge.dart'; @@ -58,6 +59,7 @@ class _RecentDmConversationsPageState extends State w final sorted = model!.sorted; return Scaffold( appBar: AppBar(title: Text(zulipLocalizations.recentDmConversationsPageTitle)), + bottomNavigationBar: ZulipNavigationBar(selectedPage: RecentDmConversationsPage), body: SafeArea( // Don't pad the bottom here; we want the list content to do that. bottom: false, diff --git a/lib/widgets/subscription_list.dart b/lib/widgets/subscription_list.dart index 9769311543..899f942f23 100644 --- a/lib/widgets/subscription_list.dart +++ b/lib/widgets/subscription_list.dart @@ -6,6 +6,7 @@ import '../model/narrow.dart'; import '../model/unreads.dart'; import 'icons.dart'; import 'message_list.dart'; +import 'navigation_bar.dart'; import 'page.dart'; import 'store.dart'; import 'text.dart'; @@ -82,6 +83,7 @@ class _SubscriptionListPageState extends State with PerAcc return Scaffold( appBar: AppBar(title: const Text("Streams")), + bottomNavigationBar: ZulipNavigationBar(selectedPage: SubscriptionListPage), body: SafeArea( // Don't pad the bottom here; we want the list content to do that. bottom: false, diff --git a/test/widgets/inbox_test.dart b/test/widgets/inbox_test.dart index b3cda6613a..f826c92749 100644 --- a/test/widgets/inbox_test.dart +++ b/test/widgets/inbox_test.dart @@ -45,7 +45,7 @@ void main() { navigatorObservers: [if (navigatorObserver != null) navigatorObserver], home: PerAccountStoreWidget( accountId: eg.selfAccount.id, - child: const InboxPage())))); + child: const Inbox())))); // global store and per-account store get loaded await tester.pumpAndSettle(); diff --git a/test/widgets/subscription_list_test.dart b/test/widgets/subscription_list_test.dart index 1259bca4a4..2689519786 100644 --- a/test/widgets/subscription_list_test.dart +++ b/test/widgets/subscription_list_test.dart @@ -154,7 +154,9 @@ void main() { subscription, ], unreadMsgs: unreadMsgs); check(getItemCount()).equals(1); - check(tester.widget(find.byType(Icon)).color) + final scaffoldBody = tester.widget(find.byType(Scaffold)).body; + check(tester.widget(find.descendant(of: + find.byWidget(scaffoldBody!), matching: find.byType(Icon))).color) .equals(swatch.iconOnPlainBackground); check(tester.widget(find.byType(UnreadCountBadge)).backgroundColor) .equals(swatch);