diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 81227c202..52370f667 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -84,5 +84,12 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + diff --git a/mobile/lib/features/home/home_page/wide_navigation.dart b/mobile/lib/features/home/home_page/wide_navigation.dart index 0c8e4027e..aa96ca80c 100644 --- a/mobile/lib/features/home/home_page/wide_navigation.dart +++ b/mobile/lib/features/home/home_page/wide_navigation.dart @@ -7,7 +7,7 @@ class _WideNavigationSidebar extends HookConsumerWidget { final String? selectedChannelId; final VoidCallback onProfileSelected; final bool isCommunitySwitching; - final ValueChanged onCommunitySwitchStart; + final ValueChanged onCommunitySwitchStart; final List<_HomeDestination> destinations; const _WideNavigationSidebar({ @@ -285,7 +285,7 @@ class _WideNavigationSectionLabel extends StatelessWidget { class _WideNavigationCommunitySwitcher extends HookConsumerWidget { final VoidCallback onTap; - final ValueChanged onCommunitySwitchStart; + final ValueChanged onCommunitySwitchStart; const _WideNavigationCommunitySwitcher({ required this.onTap, @@ -350,9 +350,19 @@ class _WideNavigationCommunitySwitcher extends HookConsumerWidget { if (!context.mounted) return; onCommunitySwitchStart(nextCommunity.id); unawaited(HapticFeedback.selectionClick()); - await ref - .read(communityListProvider.notifier) - .switchCommunity(nextCommunity.id); + try { + await ref + .read(communityListProvider.notifier) + .switchCommunity(nextCommunity.id); + } catch (error) { + debugPrint('[CommunitySwitcher] failed to switch community: $error'); + if (!context.mounted) return; + onCommunitySwitchStart(null); + swipeProgress.value = 0; + switchTarget.value = null; + isSwitching.value = false; + return; + } if (!context.mounted) return; swipeProgress.value = 0; switchTarget.value = null; diff --git a/mobile/test/features/home/home_page_test.dart b/mobile/test/features/home/home_page_test.dart index e24384004..fc8cf847c 100644 --- a/mobile/test/features/home/home_page_test.dart +++ b/mobile/test/features/home/home_page_test.dart @@ -658,6 +658,84 @@ void main() { ); }); + testWidgets('a failed community swipe restores the workspace', ( + tester, + ) async { + tester.view.physicalSize = const Size(1180, 820); + tester.view.devicePixelRatio = 1; + addTearDown(() { + tester.view.resetPhysicalSize(); + tester.view.resetDevicePixelRatio(); + }); + final communityListNotifier = _FailingCommunityListNotifier([ + Community( + id: 'alpha', + name: 'Alpha', + relayUrl: 'wss://alpha.example.com', + addedAt: DateTime(2026), + ), + Community( + id: 'bravo', + name: 'Bravo', + relayUrl: 'wss://bravo.example.com', + addedAt: DateTime(2026), + ), + ]); + + await tester.pumpWidget( + await buildHome( + communityListNotifier: communityListNotifier, + activeCommunity: communityListNotifier.communities.first, + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 1)); + + Future swipe() async { + final gesture = tester.widget( + find.descendant( + of: find.byKey(const Key('wide-navigation-community-icon')), + matching: find.byType(GestureDetector), + ), + ); + gesture.onVerticalDragStart!( + DragStartDetails(globalPosition: Offset.zero), + ); + gesture.onVerticalDragUpdate!( + DragUpdateDetails( + globalPosition: const Offset(0, 40), + delta: const Offset(0, 40), + ), + ); + await tester.pump(); + gesture.onVerticalDragEnd!(DragEndDetails(velocity: Velocity.zero)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 120)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 120)); + await tester.pump(); + } + + await swipe(); + expect(communityListNotifier.switchAttempts, 1); + expect( + tester + .widget( + find.ancestor( + of: find.byKey( + const Key('wide-navigation-community-switch-skeleton'), + ), + matching: find.byType(SkeletonShimmer), + ), + ) + .enabled, + isFalse, + ); + + await swipe(); + expect(communityListNotifier.switchAttempts, 2); + }); + testWidgets('gives selection haptics only when the tab changes', ( tester, ) async { @@ -783,3 +861,19 @@ class _FakeCommunityListNotifier extends CommunityListNotifier { switchedToId = id; } } + +class _FailingCommunityListNotifier extends CommunityListNotifier { + _FailingCommunityListNotifier(this.communities); + + final List communities; + int switchAttempts = 0; + + @override + Future> build() async => communities; + + @override + Future switchCommunity(String id) async { + switchAttempts++; + throw Exception('Unable to save active community'); + } +}