From e1e1cd50abdc254c7e64e97d565458088971ed8f Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Mon, 9 Nov 2026 06:59:08 -0800 Subject: [PATCH] fix(mobile): keep Home's header clean under sheets (#9152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On iOS, opening a sheet over Home (for example a channel's long-press actions) left a hard horizontal edge and a light glow band under the filter chips. Home's header keeps a native UIKit soft-edge view behind its Flutter controls, and the sheet's dim barrier doesn't composite cleanly over it. While another route covers Home, the header now switches to its Flutter backdrop through the existing `FrostedAppBar.nativeViewSuppressed` hook, the same mechanism other headers use while a backdrop covers them. Home already hides its native bottom controls under the same condition. | Before | After | | --- | --- | | before | after | ### Validation - iPhone 17 Pro simulator (iOS 27), Builderlab test community: reproduced the artifact on `main` with Home scrolled and the channel actions sheet open, then confirmed it is gone with this change. Also opened and dismissed the sheet repeatedly and checked the header before, during and after. - Transition timing, frame-stepped from simulator recordings of `main` (top row of each strip) and this PR (bottom row) on the same Home state: - The header and the bottom navigation controls switch on the same frame. A modal sheet doesn't drive Home's secondary animation (`MaterialPageRoute.canTransitionTo` is false for a sheet route), so the bottom controls' `secondaryAnimation == 0` check reduces to the same `isCurrent` gate the header uses. - Known limitation: while the native header view is swapped out, the header blanks for about 2 display frames as the sheet opens and about 3 as it closes (about 30 to 50 ms, seen as the missing chips below). `main` has no blank frames but shows the hard edge. Opening: sheet open, main above PR Closing: sheet close, main above PR - Extended the iOS Home widget journey: the native header view is removed while the channel actions sheet is open and restored after it is dismissed. Without the fix, the journey fails. - Full mobile Flutter suite passes. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Tom Brow Co-authored-by: Tom Brow Co-authored-by: Claude Opus 5.5 --- mobile/lib/features/channels/channels_page.dart | 7 +++++++ mobile/test/features/home/home_page_test.dart | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/mobile/lib/features/channels/channels_page.dart b/mobile/lib/features/channels/channels_page.dart index deacde27b6..b60bd5eb66 100644 --- a/mobile/lib/features/channels/channels_page.dart +++ b/mobile/lib/features/channels/channels_page.dart @@ -192,6 +192,12 @@ class ChannelsPage extends HookConsumerWidget { : filterContentHeight, useNativeNavigation: messagesFilters == null, ); + // A sheet's dim barrier can't composite over Home's native soft edge, so + // use the Flutter backdrop while another route covers this page. + final route = ModalRoute.of(context); + final headerNativeSuppressed = useValueNotifier(false); + headerNativeSuppressed.value = + messagesFilters != null && !(route?.isCurrent ?? true); final communityAvatarKey = useMemoized(GlobalKey.new); final nativeCommunityAvatarBounds = useRef(null); final headerKey = useMemoized(GlobalKey.new); @@ -607,6 +613,7 @@ class ChannelsPage extends HookConsumerWidget { onNativeReadyChanged: (ready) => nativeHeaderReady.value = ready, nativeTitle: '', useNativeNavigation: messagesFilters == null, + nativeViewSuppressed: headerNativeSuppressed, nativeLeading: messagesFilters != null ? null : IosNavigationAction( diff --git a/mobile/test/features/home/home_page_test.dart b/mobile/test/features/home/home_page_test.dart index bca16bfc6f..c844d199fd 100644 --- a/mobile/test/features/home/home_page_test.dart +++ b/mobile/test/features/home/home_page_test.dart @@ -12,6 +12,7 @@ import 'package:buzz/features/home/messages_filters.dart'; import 'package:buzz/features/home/messages_navigation_bar.dart'; import 'package:buzz/shared/widgets/ios_message_create_button.dart'; import 'package:buzz/shared/widgets/ios_glass_navigation_button.dart'; +import 'package:buzz/shared/widgets/ios_navigation_bar.dart'; import 'package:buzz/features/search/search_page.dart'; import 'package:buzz/shared/theme/theme.dart'; import 'package:flutter/foundation.dart'; @@ -576,6 +577,20 @@ void main() { await tester.pumpAndSettle(); expect(tester.widget(homeOpacity).opacity, 1); expect(find.text('Messages'), findsOneWidget); + // A sheet's dim barrier can't composite over the native header edge. + final headerNativeView = find.descendant( + of: find.byType(FrostedAppBar), + matching: find.byType(IosNavigationBar), + ); + expect(headerNativeView, findsOneWidget); + await tester.longPress(find.text('General')); + await tester.pumpAndSettle(); + expect(find.text('Copy channel name'), findsOneWidget); + expect(headerNativeView, findsNothing); + await tester.tapAt(const Offset(20, 20)); + await tester.pumpAndSettle(); + expect(find.text('Copy channel name'), findsNothing); + expect(headerNativeView, findsOneWidget); await tester.pumpWidget(const SizedBox.shrink()); debugDefaultTargetPlatformOverride = null; },