From f3b127e3af5c14d2314faa2b63556c3f51ec6743 Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Sat, 7 Nov 2026 16:30:56 +0000 Subject: [PATCH] fix(mobile): keep Home bell tappable while scrolled (#9241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Keep the mobile Home bell tappable while scrolling. Reserve both header buttons’ space as soon as the filters overlap the toolbar, and clip filter overflow. ### Related issue No matching open issue or PR found. ### Testing - `just mobile-check` and `just mobile-test` passed. - Android/iOS regression coverage checks the bell at five scroll positions and Activity navigation. iOS uses platform-view hit testing plus its callback; no physical-device verification. - Pre-push checks passed. The additional `just ci` run passed workspace Rust lint and desktop source checks; stopped during desktop native dependency compilation, so full local CI is incomplete. Signed-off-by: kenny lopez Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> --- .../lib/features/channels/channels_page.dart | 22 ++++++++-- mobile/lib/features/home/home_page.dart | 2 + mobile/test/features/home/home_page_test.dart | 43 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/mobile/lib/features/channels/channels_page.dart b/mobile/lib/features/channels/channels_page.dart index ede8679d84..deacde27b6 100644 --- a/mobile/lib/features/channels/channels_page.dart +++ b/mobile/lib/features/channels/channels_page.dart @@ -116,6 +116,7 @@ class ChannelsPage extends HookConsumerWidget { this.onSearch, this.messagesFilters, this.messagesActionsBuilder, + this.messagesActionsWidth = 0, this.soloSliver, this.showTeam = true, this.showTeamHeading = false, @@ -133,6 +134,9 @@ class ChannelsPage extends HookConsumerWidget { final Widget Function(bool isCollapsed)? messagesFilters; final List Function(VoidCallback openSwitcher, GlobalKey avatarKey)? messagesActionsBuilder; + + /// Width of the trailing controls that collapsed filters must leave clear. + final double messagesActionsWidth; final Widget? soloSliver; final bool showTeam; final bool showTeamHeading; @@ -737,10 +741,22 @@ class ChannelsPage extends HookConsumerWidget { ? const SizedBox.expand() : Padding( padding: EdgeInsets.only( - right: 60 * headerCollapse.value / filterContentHeight, + // As soon as the rows overlap, keep the entire action + // cluster clear. Interpolating this inset blocks the bell + // during partial collapse even when the final layout fits. + right: headerCollapse.value > 0 + ? messagesActionsWidth + _kTopSectionInset + : 0, ), - child: messagesFilters!( - headerCollapse.value >= filterContentHeight, + child: ClipRect( + // Native glass paints its shadows outside the viewport, but + // scrolled filters must not paint over the header actions. + clipBehavior: headerCollapse.value > 0 + ? Clip.hardEdge + : Clip.none, + child: messagesFilters!( + headerCollapse.value >= filterContentHeight, + ), ), ), ), diff --git a/mobile/lib/features/home/home_page.dart b/mobile/lib/features/home/home_page.dart index 5c8f59ba66..cbb1b9a50d 100644 --- a/mobile/lib/features/home/home_page.dart +++ b/mobile/lib/features/home/home_page.dart @@ -99,6 +99,8 @@ class HomePage extends HookConsumerWidget { ); final messages = ChannelsPage( settingsPageBuilder: settingsPageBuilder, + messagesActionsWidth: + 2 * (defaultTargetPlatform == TargetPlatform.iOS ? 44 : 48) + Grid.xs, messagesActionsBuilder: (openSwitcher, avatarKey) { final moreActions = [ // An internal build reaches only its organization's community. diff --git a/mobile/test/features/home/home_page_test.dart b/mobile/test/features/home/home_page_test.dart index 52a8e1d87f..bca16bfc6f 100644 --- a/mobile/test/features/home/home_page_test.dart +++ b/mobile/test/features/home/home_page_test.dart @@ -342,6 +342,49 @@ void main() { } for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) { + testWidgets('$platform scrolled bell opens Activity', (tester) async { + debugDefaultTargetPlatformOverride = platform; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget(await buildHome(longSolo: true)); + await tester.pumpAndSettle(); + final scroll = tester + .widget(find.byType(CustomScrollView).first) + .controller!; + final collapseHeight = tester + .widget(find.byType(FrostedAppBar)) + .bottomHeight; + for (final fraction in [0.01, 0.25, 0.5, 0.75, 1.0]) { + scroll.jumpTo(collapseHeight * fraction); + await tester.pumpAndSettle(); + expect( + find.byTooltip('Activity').hitTestable(), + findsOneWidget, + reason: 'Bell must remain tappable at collapse fraction $fraction', + ); + } + if (platform == TargetPlatform.iOS) { + // Widget tests hit-test the platform view but cannot run UIKit's + // button action. Exercise the callback that native taps dispatch. + tester + .widget( + find.byWidgetPredicate( + (widget) => + widget is IosGlassNavigationButton && + widget.icon == IosGlassNavigationIcon.bell, + ), + ) + .onPressed!(); + } else { + await tester.tap(find.byTooltip('Activity')); + } + await tester.pumpAndSettle(); + expect(find.byType(ActivityPage), findsOneWidget); + debugDefaultTargetPlatformOverride = null; + }); + testWidgets('$platform bell follows unread inbox state', (tester) async { debugDefaultTargetPlatformOverride = platform; addTearDown(() => debugDefaultTargetPlatformOverride = null);