diff --git a/mobile/lib/features/activity/activity_page.dart b/mobile/lib/features/activity/activity_page.dart index 5cd58ae1ab..72b40eb5c1 100644 --- a/mobile/lib/features/activity/activity_page.dart +++ b/mobile/lib/features/activity/activity_page.dart @@ -1,4 +1,4 @@ -import '../home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import 'dart:async'; import 'package:flutter/foundation.dart'; diff --git a/mobile/lib/features/channels/channel_detail_page.dart b/mobile/lib/features/channels/channel_detail_page.dart index 771519e149..bb1c621ea0 100644 --- a/mobile/lib/features/channels/channel_detail_page.dart +++ b/mobile/lib/features/channels/channel_detail_page.dart @@ -1,4 +1,4 @@ -import '../home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import 'message_swipe_reply.dart'; import 'agent_reply_handoff.dart'; import 'huddle_timeline.dart'; diff --git a/mobile/lib/features/channels/channels_page.dart b/mobile/lib/features/channels/channels_page.dart index 0873952552..68dfefdd29 100644 --- a/mobile/lib/features/channels/channels_page.dart +++ b/mobile/lib/features/channels/channels_page.dart @@ -1,4 +1,4 @@ -import '../home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import 'channel_unread.dart'; import '../../shared/widgets/collapsible_section_body.dart'; import '../../shared/widgets/compose_morph_icon.dart'; diff --git a/mobile/lib/features/channels/me/me_page.dart b/mobile/lib/features/channels/me/me_page.dart index 77ecc1133a..d5c934c10d 100644 --- a/mobile/lib/features/channels/me/me_page.dart +++ b/mobile/lib/features/channels/me/me_page.dart @@ -1,4 +1,4 @@ -import '../../home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/mobile/lib/features/channels/thread_detail_page.dart b/mobile/lib/features/channels/thread_detail_page.dart index 369346263b..5de5f2086f 100644 --- a/mobile/lib/features/channels/thread_detail_page.dart +++ b/mobile/lib/features/channels/thread_detail_page.dart @@ -1,4 +1,4 @@ -import '../home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import 'message_swipe_reply.dart'; import '../../shared/widgets/ios_scroll_to_top.dart'; import 'message_footer_layout.dart'; diff --git a/mobile/lib/features/home/home_page.dart b/mobile/lib/features/home/home_page.dart index 8b1283e714..6d316577d9 100644 --- a/mobile/lib/features/home/home_page.dart +++ b/mobile/lib/features/home/home_page.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import '../../shared/community/community_provider.dart'; import '../channels/channel_detail_page.dart'; import 'dart:ui'; diff --git a/mobile/lib/features/profile/profile_avatar_crop_page.dart b/mobile/lib/features/profile/profile_avatar_crop_page.dart index 98327ad1f6..199ec3fb1e 100644 --- a/mobile/lib/features/profile/profile_avatar_crop_page.dart +++ b/mobile/lib/features/profile/profile_avatar_crop_page.dart @@ -98,11 +98,12 @@ class ProfileAvatarCropPage extends HookWidget { return () => disposed = true; }, [imageBytes]); + final strip = defaultTargetPlatform == TargetPlatform.iOS + ? (DuoToolbarScope.maybeOf(context)?.strip ?? + FoldGeometry.of(context).strip) + : null; return DuoToolbarPortal( - strip: defaultTargetPlatform == TargetPlatform.iOS - ? (DuoToolbarScope.maybeOf(context)?.strip ?? - FoldGeometry.of(context).strip) - : null, + strip: strip, active: (DuoToolbarScope.maybeOf(context)?.active ?? true) && (ModalRoute.isCurrentOf(context) ?? true), @@ -128,19 +129,22 @@ class ProfileAvatarCropPage extends HookWidget { fontWeight: FontWeight.w600, ), ), - leadingWidth: 120, - leading: Padding( - padding: const EdgeInsets.only(left: Grid.gutter), - child: Align( - alignment: Alignment.centerLeft, - child: _CropHeaderButton( - label: 'Cancel', - onPressed: isCropping.value - ? null - : () => Navigator.pop(context), - ), - ), - ), + automaticallyImplyLeading: false, + leadingWidth: strip == null ? 120 : null, + leading: strip != null + ? null + : Padding( + padding: const EdgeInsets.only(left: Grid.gutter), + child: Align( + alignment: Alignment.centerLeft, + child: _CropHeaderButton( + label: 'Cancel', + onPressed: isCropping.value + ? null + : () => Navigator.pop(context), + ), + ), + ), actions: [ Padding( padding: const EdgeInsets.only(right: Grid.gutter), diff --git a/mobile/lib/features/search/search_page.dart b/mobile/lib/features/search/search_page.dart index a7ffdb8d82..e6d46e4361 100644 --- a/mobile/lib/features/search/search_page.dart +++ b/mobile/lib/features/search/search_page.dart @@ -1,4 +1,4 @@ -import '../home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import '../../shared/widgets/duo_toolbar.dart'; import '../../shared/widgets/ios_navigation_bar.dart'; import 'package:flutter/foundation.dart'; diff --git a/mobile/lib/features/settings/settings_page.dart b/mobile/lib/features/settings/settings_page.dart index 4e0bc8928c..2a7d9bf494 100644 --- a/mobile/lib/features/settings/settings_page.dart +++ b/mobile/lib/features/settings/settings_page.dart @@ -1,4 +1,4 @@ -import '../home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import 'dart:async'; import 'package:flutter/foundation.dart'; diff --git a/mobile/lib/features/home/adaptive_conversation_pane.dart b/mobile/lib/shared/widgets/adaptive_conversation_pane.dart similarity index 98% rename from mobile/lib/features/home/adaptive_conversation_pane.dart rename to mobile/lib/shared/widgets/adaptive_conversation_pane.dart index 29ef98d869..a864dd7098 100644 --- a/mobile/lib/features/home/adaptive_conversation_pane.dart +++ b/mobile/lib/shared/widgets/adaptive_conversation_pane.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:duo_kit/duo_kit.dart'; -import '../../shared/widgets/duo_toolbar.dart'; -import '../../shared/widgets/ios_navigation_metrics.dart'; -import '../../shared/widgets/duo_workspace_scaffold.dart'; +import 'duo_toolbar.dart'; +import 'ios_navigation_metrics.dart'; +import 'duo_workspace_scaffold.dart'; -import '../../shared/theme/theme.dart'; +import '../theme/theme.dart'; /// The active vertical fold, in window coordinates, when held like a book. Rect? bookDivisionOf(BuildContext context) { diff --git a/mobile/test/features/home/home_page_test.dart b/mobile/test/features/home/home_page_test.dart index 80abff0341..809cd4b8a4 100644 --- a/mobile/test/features/home/home_page_test.dart +++ b/mobile/test/features/home/home_page_test.dart @@ -1,4 +1,4 @@ -import 'package:buzz/features/home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import 'package:buzz/features/activity/activity_page.dart'; import 'package:buzz/shared/widgets/frosted_scaffold.dart'; import 'package:buzz/shared/widgets/concentric_sheet_surface.dart'; diff --git a/mobile/test/features/profile/profile_avatar_crop_cancel_test.dart b/mobile/test/features/profile/profile_avatar_crop_cancel_test.dart new file mode 100644 index 0000000000..bdf1c145f4 --- /dev/null +++ b/mobile/test/features/profile/profile_avatar_crop_cancel_test.dart @@ -0,0 +1,96 @@ +import 'dart:async'; + +import 'package:buzz/features/profile/profile_avatar_crop_page.dart'; +import 'package:buzz/shared/widgets/duo_toolbar.dart'; +import 'package:buzz/shared/widgets/ios_navigation_bar.dart'; +import 'package:duo_kit/duo_kit.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +void main() { + for (final side in [null, FoldStripSide.left, FoldStripSide.right]) { + for (final failed in [false, true]) { + testWidgets('crop has one cancel owner: $side, failed=$failed', ( + tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + final semantics = tester.ensureSemantics(); + final bytes = Completer(); + final strip = side == null + ? null + : FoldStrip( + side: side, + rect: Rect.fromLTWH( + side == FoldStripSide.left ? 0 : 724, + 0, + 76, + 600, + ), + topClearance: 110, + bottomClearance: 0, + isClearanceEstimated: false, + ); + await tester.pumpWidget( + ProviderScope( + child: MaterialApp( + home: Builder( + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => DuoToolbarScope( + strip: strip, + active: true, + child: ProfileAvatarCropPage(imageBytes: bytes.future), + ), + ), + ), + child: const Text('Open crop'), + ), + ), + ), + ), + ); + await tester.tap(find.text('Open crop')); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 400)); + if (failed) { + bytes.completeError(StateError('Cannot load image')); + await tester.pump(); + await tester.pump(); + expect(find.text("We couldn't prepare that photo."), findsOneWidget); + } + if (side == null) { + expect(find.bySemanticsLabel('Cancel'), findsOneWidget); + expect(find.byType(IosNavigationBar), findsNothing); + await tester.tap(find.text('Cancel')); + } else { + // UIKit owns the rail's semantics; its native action payload must + // have one cancel, with no competing Flutter cancel/back control. + expect(find.bySemanticsLabel(RegExp('Cancel|Back')), findsNothing); + final header = tester.widget(find.byType(AppBar)); + expect(header.leading, isNull); + expect(header.automaticallyImplyLeading, isFalse); + final native = tester.widget(find.byType(UiKitView)); + final payload = native.creationParams! as Map; + expect( + payload['leading'], + containsPair('label', 'Cancel photo positioning'), + ); + final rail = tester.widget( + find.byType(IosNavigationBar), + ); + rail.leading!.onPressed!(); + } + await tester.pumpAndSettle(); + expect(find.byType(ProfileAvatarCropPage), findsNothing); + expect(find.text('Open crop'), findsOneWidget); + expect(tester.takeException(), isNull); + semantics.dispose(); + debugDefaultTargetPlatformOverride = null; + }); + } + } +} diff --git a/mobile/test/features/settings/settings_page_test.dart b/mobile/test/features/settings/settings_page_test.dart index 0eb5eb5f65..9c5b2bb8fe 100644 --- a/mobile/test/features/settings/settings_page_test.dart +++ b/mobile/test/features/settings/settings_page_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:buzz/features/settings/settings_page.dart'; -import 'package:buzz/features/home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import 'package:buzz/shared/community/community_membership_provider.dart'; import 'package:buzz/shared/community/community.dart'; import 'package:buzz/shared/community/community_provider.dart'; diff --git a/mobile/test/features/home/adaptive_conversation_pane_test.dart b/mobile/test/shared/widgets/adaptive_conversation_pane_test.dart similarity index 99% rename from mobile/test/features/home/adaptive_conversation_pane_test.dart rename to mobile/test/shared/widgets/adaptive_conversation_pane_test.dart index ba215ac4ad..b709f96964 100644 --- a/mobile/test/features/home/adaptive_conversation_pane_test.dart +++ b/mobile/test/shared/widgets/adaptive_conversation_pane_test.dart @@ -7,7 +7,7 @@ import 'package:duo_kit/duo_kit.dart'; import 'package:buzz/shared/widgets/frosted_scaffold.dart'; import 'package:buzz/shared/widgets/frosted_app_bar.dart'; import 'package:buzz/shared/widgets/modal_presentation.dart'; -import 'package:buzz/features/home/adaptive_conversation_pane.dart'; +import 'package:buzz/shared/widgets/adaptive_conversation_pane.dart'; import 'package:buzz/shared/theme/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart';