fix(mobile): keep Home bell tappable while scrolled (#9241)

## 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 <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
This commit is contained in:
klopez4212
2026-11-07 16:30:56 +00:00
committed by GitHub
parent 12f3d51001
commit f3b127e3af
3 changed files with 64 additions and 3 deletions

View File

@@ -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<Widget> 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,
),
),
),
),

View File

@@ -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.

View File

@@ -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<CustomScrollView>(find.byType(CustomScrollView).first)
.controller!;
final collapseHeight = tester
.widget<FrostedAppBar>(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<IosGlassNavigationButton>(
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);