Skip to content

Commit 3158bc4

Browse files
authored
Move SnackBars to top to avoid blocking bottom navigation (#413)
* Move SnackBars to top to avoid blocking bottom navigation - Display SnackBars at the top of the screen using floating behavior - Prevent SnackBars from overlapping the bottom navigation bar - Allow uninterrupted navigation while notifications are visible - Centralize SnackBar configuration via a shared helper - Ensure consistent notification behavior across the app * fix test * replace hardcoded margins with adaptive layout - Remove hardcoded pixel values flagged in code review - Use system-provided metrics for layout calculations - Prevent multiple SnackBars from stacking * centralize SnackBar logic with async-safe method * Clamp bottom margin to prevent negative EdgeInsets values
1 parent 8551b5e commit 3158bc4

23 files changed

+251
-177
lines changed

lib/core/deep_link_handler.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:logger/logger.dart';
66
import 'package:mostro_mobile/generated/l10n.dart';
77
import 'package:mostro_mobile/services/deep_link_service.dart';
88
import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart';
9+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
910

1011
class DeepLinkHandler {
1112
final Ref _ref;
@@ -149,12 +150,11 @@ class DeepLinkHandler {
149150
void _showErrorSnackBar(BuildContext? context, String message) {
150151
if (context == null) return;
151152

152-
ScaffoldMessenger.of(context).showSnackBar(
153-
SnackBar(
154-
content: Text(message),
155-
backgroundColor: Colors.red,
156-
duration: const Duration(seconds: 3),
157-
),
153+
SnackBarHelper.showTopSnackBar(
154+
context,
155+
message,
156+
duration: const Duration(seconds: 3),
157+
backgroundColor: Colors.red,
158158
);
159159
}
160160

lib/features/auth/screens/login_screen.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:mostro_mobile/features/auth/notifiers/auth_state.dart';
77
import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart';
88
import 'package:mostro_mobile/shared/widgets/custom_button.dart';
99
import 'package:mostro_mobile/generated/l10n.dart';
10+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
1011

1112
class LoginScreen extends HookConsumerWidget {
1213
const LoginScreen({super.key});
@@ -20,8 +21,9 @@ class LoginScreen extends HookConsumerWidget {
2021
if (state is AuthAuthenticated) {
2122
context.go('/');
2223
} else if (state is AuthFailure) {
23-
ScaffoldMessenger.of(context).showSnackBar(
24-
SnackBar(content: Text(state.error)),
24+
SnackBarHelper.showTopSnackBar(
25+
context,
26+
state.error,
2527
);
2628
}
2729
});

lib/features/auth/screens/register_screen.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:mostro_mobile/features/notifications/providers/backup_reminder_p
99
import 'package:mostro_mobile/shared/widgets/custom_button.dart';
1010
import 'package:mostro_mobile/shared/utils/nostr_utils.dart';
1111
import 'package:mostro_mobile/generated/l10n.dart';
12+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
1213

1314
class RegisterScreen extends HookConsumerWidget {
1415
const RegisterScreen({super.key});
@@ -39,8 +40,9 @@ class RegisterScreen extends HookConsumerWidget {
3940
// Navigate to home after successful registration
4041
context.go('/');
4142
} else if (state is AuthFailure) {
42-
ScaffoldMessenger.of(context).showSnackBar(
43-
SnackBar(content: Text(state.error)),
43+
SnackBarHelper.showTopSnackBar(
44+
context,
45+
state.error,
4446
);
4547
}
4648
});

lib/features/chat/widgets/encrypted_file_message.dart

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart';
1111
import 'package:mostro_mobile/services/encrypted_file_upload_service.dart';
1212
import 'package:mostro_mobile/services/file_validation_service.dart';
1313
import 'package:mostro_mobile/generated/l10n.dart';
14+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
1415

1516
class EncryptedFileMessage extends ConsumerStatefulWidget {
1617
final NostrEvent message;
@@ -494,22 +495,20 @@ class _EncryptedFileMessageState extends ConsumerState<EncryptedFileMessage> {
494495

495496
if (mounted) {
496497
if (result.type != ResultType.done) {
497-
ScaffoldMessenger.of(context).showSnackBar(
498-
SnackBar(
499-
content: Text('Could not open file: ${result.message}'),
500-
backgroundColor: Colors.orange,
501-
duration: const Duration(seconds: 3),
502-
),
498+
SnackBarHelper.showTopSnackBar(
499+
context,
500+
'Could not open file: ${result.message}',
501+
duration: const Duration(seconds: 3),
502+
backgroundColor: Colors.orange,
503503
);
504504
}
505505
}
506506
} catch (e) {
507507
if (mounted) {
508-
ScaffoldMessenger.of(context).showSnackBar(
509-
SnackBar(
510-
content: Text('Error opening file: $e'),
511-
backgroundColor: Colors.red,
512-
),
508+
SnackBarHelper.showTopSnackBar(
509+
context,
510+
'Error opening file: $e',
511+
backgroundColor: Colors.red,
513512
);
514513
}
515514
}

lib/features/chat/widgets/encrypted_image_message.dart

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:mostro_mobile/core/app_theme.dart';
1111
import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart';
1212
import 'package:mostro_mobile/services/encrypted_image_upload_service.dart';
1313
import 'package:mostro_mobile/generated/l10n.dart';
14+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
1415

1516
class EncryptedImageMessage extends ConsumerStatefulWidget {
1617
final NostrEvent message;
@@ -373,22 +374,20 @@ class _EncryptedImageMessageState extends ConsumerState<EncryptedImageMessage> {
373374

374375
if (mounted) {
375376
if (result.type != ResultType.done) {
376-
ScaffoldMessenger.of(context).showSnackBar(
377-
SnackBar(
378-
content: Text('$couldNotOpenMsg: ${result.message}'),
379-
backgroundColor: Colors.orange,
380-
duration: const Duration(seconds: 3),
381-
),
377+
SnackBarHelper.showTopSnackBar(
378+
context,
379+
'$couldNotOpenMsg: ${result.message}',
380+
duration: const Duration(seconds: 3),
381+
backgroundColor: Colors.orange,
382382
);
383383
}
384384
}
385385
} catch (e) {
386386
if (mounted) {
387-
ScaffoldMessenger.of(context).showSnackBar(
388-
SnackBar(
389-
content: Text('$errorOpeningMsg: $e'),
390-
backgroundColor: Colors.red,
391-
),
387+
SnackBarHelper.showTopSnackBar(
388+
context,
389+
'$errorOpeningMsg: $e',
390+
backgroundColor: Colors.red,
392391
);
393392
}
394393
}

lib/features/chat/widgets/message_bubble.dart

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:mostro_mobile/shared/providers/avatar_provider.dart';
88
import 'package:mostro_mobile/features/chat/widgets/encrypted_image_message.dart';
99
import 'package:mostro_mobile/features/chat/widgets/encrypted_file_message.dart';
1010
import 'package:mostro_mobile/features/chat/utils/message_type_helpers.dart';
11+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
1112

1213
class MessageBubble extends ConsumerWidget {
1314
final NostrEvent message;
@@ -154,17 +155,11 @@ class MessageBubble extends ConsumerWidget {
154155
// Only copy if text is not empty
155156
if (text.isNotEmpty) {
156157
Clipboard.setData(ClipboardData(text: text));
157-
ScaffoldMessenger.of(context).showSnackBar(
158-
SnackBar(
159-
content: Text(S.of(context)!.messageCopiedToClipboard),
160-
duration: const Duration(seconds: 2),
161-
backgroundColor: AppTheme.backgroundCard,
162-
behavior: SnackBarBehavior.floating,
163-
margin: const EdgeInsets.all(16),
164-
shape: RoundedRectangleBorder(
165-
borderRadius: BorderRadius.circular(8),
166-
),
167-
),
158+
SnackBarHelper.showTopSnackBar(
159+
context,
160+
S.of(context)!.messageCopiedToClipboard,
161+
duration: const Duration(seconds: 2),
162+
backgroundColor: AppTheme.backgroundCard,
168163
);
169164
}
170165
}

lib/features/chat/widgets/message_input.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:mostro_mobile/services/encrypted_file_upload_service.dart';
1212
import 'package:mostro_mobile/services/encrypted_image_upload_service.dart';
1313
import 'package:mostro_mobile/services/file_validation_service.dart';
1414
import 'package:mostro_mobile/services/media_validation_service.dart';
15+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
1516

1617
class MessageInput extends ConsumerStatefulWidget {
1718
final String orderId;
@@ -127,11 +128,10 @@ class _MessageInputState extends ConsumerState<MessageInput> {
127128
} catch (e) {
128129
// Show error to user
129130
if (mounted) {
130-
ScaffoldMessenger.of(context).showSnackBar(
131-
SnackBar(
132-
content: Text('Error uploading file: $e'),
133-
backgroundColor: Colors.red,
134-
),
131+
SnackBarHelper.showTopSnackBar(
132+
context,
133+
'Error uploading file: $e',
134+
backgroundColor: Colors.red,
135135
);
136136
}
137137
} finally {

lib/features/disputes/widgets/dispute_input_section.dart

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:mostro_mobile/core/app_theme.dart';
33
import 'package:mostro_mobile/generated/l10n.dart';
4+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
45

56
class DisputeInputSection extends StatefulWidget {
67
final String disputeId;
@@ -127,20 +128,18 @@ class _DisputeInputSectionState extends State<DisputeInputSection> {
127128
await Future.delayed(const Duration(milliseconds: 500));
128129

129130
if (mounted) {
130-
ScaffoldMessenger.of(context).showSnackBar(
131-
SnackBar(
132-
content: Text('Message sent: $message'),
133-
backgroundColor: Colors.green,
134-
),
131+
SnackBarHelper.showTopSnackBar(
132+
context,
133+
'Message sent: $message',
134+
backgroundColor: Colors.green,
135135
);
136136
}
137137
} catch (error) {
138138
if (mounted) {
139-
ScaffoldMessenger.of(context).showSnackBar(
140-
SnackBar(
141-
content: Text('Failed to send message: $error'),
142-
backgroundColor: Colors.red,
143-
),
139+
SnackBarHelper.showTopSnackBar(
140+
context,
141+
'Failed to send message: $error',
142+
backgroundColor: Colors.red,
144143
);
145144
}
146145
} finally {

lib/features/disputes/widgets/dispute_message_bubble.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:flutter/services.dart';
33
import 'package:mostro_mobile/core/app_theme.dart';
44
import 'package:mostro_mobile/generated/l10n.dart';
5+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
56

67
class DisputeMessageBubble extends StatelessWidget {
78
final String message;
@@ -88,12 +89,11 @@ class DisputeMessageBubble extends StatelessWidget {
8889

8990
void _copyToClipboard(BuildContext context, String text) {
9091
Clipboard.setData(ClipboardData(text: text));
91-
ScaffoldMessenger.of(context).showSnackBar(
92-
SnackBar(
93-
content: Text(S.of(context)?.messageCopiedToClipboard ?? 'Message copied to clipboard'),
94-
duration: const Duration(seconds: 1),
95-
backgroundColor: Colors.green,
96-
),
92+
SnackBarHelper.showTopSnackBar(
93+
context,
94+
S.of(context)?.messageCopiedToClipboard ?? 'Message copied to clipboard',
95+
duration: const Duration(seconds: 1),
96+
backgroundColor: Colors.green,
9797
);
9898
}
9999

lib/features/key_manager/key_management_screen.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:mostro_mobile/features/notifications/providers/backup_reminder_p
1313
import 'package:mostro_mobile/shared/providers.dart';
1414
import 'package:mostro_mobile/generated/l10n.dart';
1515
import 'package:mostro_mobile/shared/providers/notifications_history_repository_provider.dart';
16+
import 'package:mostro_mobile/shared/utils/snack_bar_helper.dart';
1617

1718
class KeyManagementScreen extends ConsumerStatefulWidget {
1819
const KeyManagementScreen({super.key});
@@ -90,14 +91,16 @@ class _KeyManagementScreenState extends ConsumerState<KeyManagementScreen> {
9091
await keyManager.importMnemonic(importValue);
9192
await _loadKeys();
9293
if (mounted) {
93-
ScaffoldMessenger.of(context).showSnackBar(
94-
SnackBar(content: Text(S.of(context)!.keyImportedSuccessfully)),
94+
SnackBarHelper.showTopSnackBar(
95+
context,
96+
S.of(context)!.keyImportedSuccessfully,
9597
);
9698
}
9799
} catch (e) {
98100
if (mounted) {
99-
ScaffoldMessenger.of(context).showSnackBar(
100-
SnackBar(content: Text(S.of(context)!.importFailed(e.toString()))),
101+
SnackBarHelper.showTopSnackBar(
102+
context,
103+
S.of(context)!.importFailed(e.toString()),
101104
);
102105
}
103106
}

0 commit comments

Comments
 (0)