From 5c95a768e0f5082bd1bc9f7d6061bccea16256d0 Mon Sep 17 00:00:00 2001 From: Strycher Date: Sat, 18 Jul 2026 03:18:09 -0400 Subject: [PATCH] fix(#289): make the nav panel pinnable inside a channel too The channel chat screen had a plain Drawer bolted onto it, so the panel could only be pinned open on the four primary views. Pinning it while reading a channel is the wide-screen case that was actually asked for: keeping the channel list and its unread counts visible without pulling it out. - AppShell now serves pushed detail screens as well: selectedIndex and onDestinationSelected are optional, and the bottom bar is omitted when they are absent (a pushed chat screen has no bottom bar). - New appBarBuilder(context, pinned) lets a screen vary its app bar with the dock state. ChannelChatScreen uses it to drop its hamburger when the panel is already pinned open, since there is nothing left to summon. - ChannelChatScreen builds on AppShell instead of a bare Scaffold, so it gets the same transient/pinned layout as everything else. The four primary views are untouched; the new parameters are additive. flutter analyze clean, dart format applied. Not yet run on hardware. --- lib/screens/channel_chat_screen.dart | 42 +++++++++++++++------------- lib/widgets/app_shell.dart | 36 ++++++++++++++++++------ 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index b4bfcd8..22c05f7 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -28,6 +28,7 @@ import '../services/block_service.dart'; import '../services/chat_text_scale_service.dart'; import '../services/translation_service.dart'; import '../utils/emoji_utils.dart'; +import '../widgets/app_shell.dart'; import '../widgets/channel_drawer_list.dart'; import '../widgets/mention_autocomplete.dart'; import '../helpers/emoji_shortcodes.dart'; @@ -294,27 +295,28 @@ class _ChannelChatScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - drawer: Drawer( - width: 300, - child: SafeArea( - child: ChannelDrawerList( - currentChannelIndex: widget.channel.index, - onChannelSelected: _switchChannel, - ), - ), + return AppShell( + // No bottom bar: this is a pushed detail screen. The panel is still + // pinnable here, which is the wide-screen point - keeping the channel + // list and its unread counts visible while reading a channel. + drawerContent: ChannelDrawerList( + currentChannelIndex: widget.channel.index, + onChannelSelected: _switchChannel, ), - appBar: AppBar( - // Explicit hamburger: this is a pushed route, so Scaffold would show a - // back arrow here instead of the drawer button. System back still pops - // to the channel list. - leading: Builder( - builder: (context) => IconButton( - icon: const Icon(Icons.menu), - tooltip: context.l10n.channels_title, - onPressed: () => Scaffold.of(context).openDrawer(), - ), - ), + appBarBuilder: (context, pinned) => AppBar( + // Pinned: the panel is already docked, so no hamburger is needed. + // Unpinned: this is a pushed route, so Scaffold would put a back arrow + // in this slot; the explicit hamburger replaces it. System back still + // pops to the channel list. + leading: pinned + ? null + : Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.menu), + tooltip: context.l10n.channels_title, + onPressed: () => Scaffold.of(context).openDrawer(), + ), + ), title: Row( children: [ _channelIcon(widget.channel), diff --git a/lib/widgets/app_shell.dart b/lib/widgets/app_shell.dart index 73680c0..cfc8ab9 100644 --- a/lib/widgets/app_shell.dart +++ b/lib/widgets/app_shell.dart @@ -15,12 +15,21 @@ class AppShell extends StatelessWidget { static const double wideBreakpoint = 720; static const double _drawerWidth = 300; - final int selectedIndex; - final ValueChanged onDestinationSelected; + /// Bottom bar tab. Null on pushed detail screens (a channel chat), which + /// carry the nav panel but no bottom bar. + final int? selectedIndex; + final ValueChanged? onDestinationSelected; final int contactsUnreadCount; final int channelsUnreadCount; final PreferredSizeWidget? appBar; + + /// App bar that needs to know whether the panel is docked, so a pushed + /// screen can drop its hamburger when the panel is already pinned open. + /// Takes precedence over [appBar]. + final PreferredSizeWidget Function(BuildContext context, bool pinned)? + appBarBuilder; + final Widget body; final Widget? floatingActionButton; @@ -29,10 +38,11 @@ class AppShell extends StatelessWidget { const AppShell({ super.key, - required this.selectedIndex, - required this.onDestinationSelected, required this.body, + this.selectedIndex, + this.onDestinationSelected, this.appBar, + this.appBarBuilder, this.floatingActionButton, this.drawerContent, this.contactsUnreadCount = 0, @@ -54,22 +64,30 @@ class AppShell extends StatelessWidget { ); } - Widget _bottomBar() { + /// Null on detail screens, which show the panel but no bottom bar. + Widget? _bottomBar() { + final index = selectedIndex; + final onSelected = onDestinationSelected; + if (index == null || onSelected == null) return null; return SafeArea( top: false, child: QuickSwitchBar( - selectedIndex: selectedIndex, - onDestinationSelected: onDestinationSelected, + selectedIndex: index, + onDestinationSelected: onSelected, contactsUnreadCount: contactsUnreadCount, channelsUnreadCount: channelsUnreadCount, ), ); } + PreferredSizeWidget? _appBar(BuildContext context, bool pinned) { + return appBarBuilder?.call(context, pinned) ?? appBar; + } + /// Wide + pinned: the panel is laid out beside the body, not overlaid. Widget _buildPinned(BuildContext context) { return Scaffold( - appBar: appBar, + appBar: _appBar(context, true), body: SafeArea( top: false, child: Row( @@ -93,7 +111,7 @@ class AppShell extends StatelessWidget { /// the hamburger into the app bar automatically. Widget _buildTransient(BuildContext context, bool isWide) { return Scaffold( - appBar: appBar, + appBar: _appBar(context, false), drawer: Drawer( width: _drawerWidth, child: _NavPanel(isWide: isWide, content: drawerContent),