From dc1e4ec14d8f5cbc2db7117ebd7a004d7add8d31 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:45:09 +0530 Subject: [PATCH] feat(navigation): Add configurable tablet drawer behavior --- lib/features/chat/views/chat_page.dart | 73 ++++++++++--------- .../navigation/widgets/chats_drawer.dart | 9 ++- .../widgets/responsive_drawer_layout.dart | 61 +++++++++++++--- 3 files changed, 96 insertions(+), 47 deletions(-) diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 51d85d4..4ebcc31 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1331,41 +1331,44 @@ class _ChatPageState extends ConsumerState { ), onPressed: _clearSelection, ) - : (isTablet - ? null // Hide menu button on tablets (drawer is always visible) - : Builder( - builder: (ctx) => Padding( - padding: const EdgeInsets.only( - left: Spacing.inputPadding, - ), - child: IconButton( - onPressed: () { - // Suppress auto-focus and dismiss keyboard, then open drawer - try { - ref - .read( - composerAutofocusEnabledProvider - .notifier, - ) - .set(false); - FocusManager.instance.primaryFocus - ?.unfocus(); - SystemChannels.textInput.invokeMethod( - 'TextInput.hide', - ); - } catch (_) {} - ResponsiveDrawerLayout.of(ctx)?.open(); - }, - icon: Icon( - Platform.isIOS - ? CupertinoIcons.line_horizontal_3 - : Icons.menu, - color: context.conduitTheme.textPrimary, - size: IconSize.appBar, - ), - ), - ), - )), + : Builder( + builder: (ctx) => Padding( + padding: const EdgeInsets.only( + left: Spacing.inputPadding, + ), + child: IconButton( + onPressed: () { + final layout = ResponsiveDrawerLayout.of(ctx); + if (layout == null) return; + + final isDrawerOpen = layout.isOpen; + if (!isDrawerOpen) { + try { + ref + .read( + composerAutofocusEnabledProvider + .notifier, + ) + .set(false); + FocusManager.instance.primaryFocus + ?.unfocus(); + SystemChannels.textInput.invokeMethod( + 'TextInput.hide', + ); + } catch (_) {} + } + layout.toggle(); + }, + icon: Icon( + Platform.isIOS + ? CupertinoIcons.line_horizontal_3 + : Icons.menu, + color: context.conduitTheme.textPrimary, + size: IconSize.appBar, + ), + ), + ), + ), title: _isSelectionMode ? Text( '${_selectedMessageIds.length} selected', diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 60e9fea..18cdebd 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -1438,9 +1438,14 @@ class _ChatsDrawerState extends ConsumerState { container.read(chat.chatMessagesProvider.notifier).clearMessages(); // Close the slide drawer for faster perceived performance - // (only on mobile; on tablet, drawer stays visible) + // (only on mobile; keep tablet drawer unless user toggles it) if (mounted) { - ResponsiveDrawerLayout.of(context)?.close(); + final mediaQuery = MediaQuery.maybeOf(context); + final isTablet = + mediaQuery != null && mediaQuery.size.shortestSide >= 600; + if (!isTablet) { + ResponsiveDrawerLayout.of(context)?.close(); + } } // Load the full conversation details in the background diff --git a/lib/shared/widgets/responsive_drawer_layout.dart b/lib/shared/widgets/responsive_drawer_layout.dart index c74b40a..27b7422 100644 --- a/lib/shared/widgets/responsive_drawer_layout.dart +++ b/lib/shared/widgets/responsive_drawer_layout.dart @@ -8,6 +8,7 @@ import '../../shared/theme/theme_extensions.dart'; /// /// On tablets (shortestSide >= 600), the drawer is always visible alongside /// the content. On mobile, it behaves like a standard slide drawer. +/// Tablets can optionally dismiss the docked drawer to reclaim space. class ResponsiveDrawerLayout extends StatefulWidget { final Widget child; final Widget drawer; @@ -26,6 +27,8 @@ class ResponsiveDrawerLayout extends StatefulWidget { // Tablet-specific configuration final double tabletDrawerWidth; // Fixed width for tablet drawer + final bool tabletDismissible; + final bool tabletInitiallyDocked; const ResponsiveDrawerLayout({ super.key, @@ -42,6 +45,8 @@ class ResponsiveDrawerLayout extends StatefulWidget { this.contentBlurSigma = 2.0, this.onOpenStart, this.tabletDrawerWidth = 320.0, + this.tabletDismissible = true, + this.tabletInitiallyDocked = true, }); static ResponsiveDrawerLayoutState? of(BuildContext context) => @@ -58,6 +63,7 @@ class ResponsiveDrawerLayoutState extends State duration: widget.duration, value: 0.0, ); + late bool _isTabletDocked = widget.tabletInitiallyDocked; bool _isTablet(BuildContext context) { final size = MediaQuery.of(context).size; @@ -73,7 +79,20 @@ class ResponsiveDrawerLayoutState extends State double get _edgeWidth => MediaQuery.of(context).size.width * widget.edgeFraction; - bool get isOpen => _controller.value == 1.0; + bool get isOpen => + _isTablet(context) ? _isTabletDocked : _controller.value == 1.0; + + @override + void didUpdateWidget(covariant ResponsiveDrawerLayout oldWidget) { + super.didUpdateWidget(oldWidget); + if (!widget.tabletDismissible && !_isTabletDocked) { + setState(() => _isTabletDocked = true); + } else if (widget.tabletInitiallyDocked != + oldWidget.tabletInitiallyDocked && + _isTablet(context)) { + setState(() => _isTabletDocked = widget.tabletInitiallyDocked); + } + } Future _animateTo( double target, { @@ -99,8 +118,12 @@ class ResponsiveDrawerLayoutState extends State } void open({double velocity = 0.0}) { - // Only animate on mobile; on tablet, drawer is always visible - if (_isTablet(context)) return; + if (_isTablet(context)) { + if (!_isTabletDocked) { + setState(() => _isTabletDocked = true); + } + return; + } try { widget.onOpenStart?.call(); @@ -110,15 +133,23 @@ class ResponsiveDrawerLayoutState extends State } void close({double velocity = 0.0}) { - // Only animate on mobile; on tablet, drawer is always visible - if (_isTablet(context)) return; + if (_isTablet(context)) { + if (!widget.tabletDismissible) return; + if (_isTabletDocked) { + setState(() => _isTabletDocked = false); + } + return; + } _animateTo(0.0, velocity: velocity, easeOut: true); } void toggle() { - // Only toggle on mobile; on tablet, drawer is always visible - if (_isTablet(context)) return; + if (_isTablet(context)) { + if (!widget.tabletDismissible) return; + setState(() => _isTabletDocked = !_isTabletDocked); + return; + } isOpen ? close() : open(); } @@ -189,18 +220,28 @@ class ResponsiveDrawerLayoutState extends State } Widget _buildTabletLayout(ConduitThemeExtension theme) { + final targetWidth = widget.tabletDismissible && !_isTabletDocked + ? 0.0 + : widget.tabletDrawerWidth; return Row( children: [ // Persistent drawer - Container( - width: widget.tabletDrawerWidth, + AnimatedContainer( + duration: widget.duration, + curve: widget.curve, + width: targetWidth, decoration: BoxDecoration( color: theme.surfaceBackground, border: Border( right: BorderSide(color: theme.dividerColor, width: 1), ), ), - child: widget.drawer, + child: ClipRect( + child: IgnorePointer( + ignoring: widget.tabletDismissible && !_isTabletDocked, + child: widget.drawer, + ), + ), ), // Content Expanded(child: widget.child),