diff --git a/lib/core/services/push_notification_service.dart b/lib/core/services/push_notification_service.dart index 45d9c51..b0283e4 100644 --- a/lib/core/services/push_notification_service.dart +++ b/lib/core/services/push_notification_service.dart @@ -29,6 +29,10 @@ class PushNotificationService { /// Callback when user taps a notification — set by the app to navigate. void Function(String? actionUrl)? onNotificationTap; + /// The conversation ID currently being viewed — suppress message push + /// notifications for this conversation to avoid self-notifications. + String? activeConversationId; + /// Initialize Firebase Messaging and local notifications. Future initialize() async { if (_initialized) return; @@ -159,6 +163,14 @@ class PushNotificationService { final notification = message.notification; if (notification == null) return; + // Suppress message push notifications when user is viewing that conversation + final msgType = message.data['type'] as String?; + final convId = message.data['conversationId'] as String?; + if (msgType == 'message' && convId != null && convId == activeConversationId) { + debugPrint('[FCM] Suppressed notification — user is viewing conversation $convId'); + return; + } + // Show local notification on Android (iOS handles it natively) if (Platform.isAndroid) { _localNotifications?.show( diff --git a/lib/core/widgets/app_bottom_nav_bar.dart b/lib/core/widgets/app_bottom_nav_bar.dart index 9f44974..f622355 100644 --- a/lib/core/widgets/app_bottom_nav_bar.dart +++ b/lib/core/widgets/app_bottom_nav_bar.dart @@ -15,7 +15,8 @@ int _currentTabIndex(BuildContext context) { return 1; } if (location.startsWith('/profile')) return 3; - return 0; // home and everything else + if (location == '/home') return 0; + return -1; // other pages (about, contact, faq, etc.) — no tab highlighted } class AppBottomNavBar extends ConsumerWidget { diff --git a/lib/features/about/presentation/screens/about_screen.dart b/lib/features/about/presentation/screens/about_screen.dart index bec583f..6728dfe 100644 --- a/lib/features/about/presentation/screens/about_screen.dart +++ b/lib/features/about/presentation/screens/about_screen.dart @@ -314,7 +314,36 @@ class AboutScreen extends ConsumerWidget { physics: const AlwaysScrollableScrollPhysics(), child: Column( children: [ - const SizedBox(height: 18), + // Back button + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () { + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } else { + context.go('/home'); + } + }, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.primaryDark.withValues(alpha: 0.08), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.arrow_back_ios_new_rounded, + color: AppColors.primaryDark, + size: 18, + ), + ), + ), + ), + ), + const SizedBox(height: 10), _buildHeroTitle(state.hero.headline), const SizedBox(height: 16), _buildHeroSubtitle(state.hero.description), diff --git a/lib/features/contact/presentation/screens/contact_screen.dart b/lib/features/contact/presentation/screens/contact_screen.dart index 8dab679..b5dd69d 100644 --- a/lib/features/contact/presentation/screens/contact_screen.dart +++ b/lib/features/contact/presentation/screens/contact_screen.dart @@ -31,7 +31,36 @@ class _ContactScreenState extends State { return ListView( padding: EdgeInsets.zero, children: [ - const SizedBox(height: 18), + // Back button + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), + child: Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () { + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } else { + context.go('/home'); + } + }, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.primaryDark.withValues(alpha: 0.08), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.arrow_back_ios_new_rounded, + color: AppColors.primaryDark, + size: 18, + ), + ), + ), + ), + ), + const SizedBox(height: 10), _buildTitle(), const SizedBox(height: 10), _buildDescription(), diff --git a/lib/features/faq/presentation/screens/faq_screen.dart b/lib/features/faq/presentation/screens/faq_screen.dart index 83b04c1..dbe6e8b 100644 --- a/lib/features/faq/presentation/screens/faq_screen.dart +++ b/lib/features/faq/presentation/screens/faq_screen.dart @@ -188,8 +188,38 @@ class FaqScreen extends ConsumerWidget { } return ListView( - padding: const EdgeInsets.fromLTRB(0, 20, 0, 30), + padding: const EdgeInsets.fromLTRB(0, 8, 0, 30), children: [ + // Back button + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () { + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } else { + context.go('/home'); + } + }, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.primaryDark.withValues(alpha: 0.08), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.arrow_back_ios_new_rounded, + color: AppColors.primaryDark, + size: 18, + ), + ), + ), + ), + ), + const SizedBox(height: 12), // Title const Padding( padding: EdgeInsets.symmetric(horizontal: 24), diff --git a/lib/features/home/presentation/widgets/top_professionals_section.dart b/lib/features/home/presentation/widgets/top_professionals_section.dart index c41d56b..6f13032 100644 --- a/lib/features/home/presentation/widgets/top_professionals_section.dart +++ b/lib/features/home/presentation/widgets/top_professionals_section.dart @@ -317,7 +317,6 @@ class _TopProfessionalsSectionState height: 226, fit: BoxFit.cover, alignment: Alignment.topCenter, - placeholder: (_) => _buildImagePlaceholder(index), errorWidget: (_) => _buildImagePlaceholder(index), ) : _buildImagePlaceholder(index), diff --git a/lib/features/messaging/presentation/providers/messaging_provider.dart b/lib/features/messaging/presentation/providers/messaging_provider.dart index 663610f..e6a09aa 100644 --- a/lib/features/messaging/presentation/providers/messaging_provider.dart +++ b/lib/features/messaging/presentation/providers/messaging_provider.dart @@ -100,6 +100,10 @@ class MessagingNotifier extends StateNotifier { // Avoid duplicates (broadcast may arrive after ack for sender's own message) if (currentMessages.any((m) => m.id == message.id)) return; + // Ignore own messages — the sender path handles these via sendMessage() + final isMine = message.senderId == _currentUserId; + if (isMine) return; + currentMessages.insert(0, message); final updatedMessages = Map>.from(state.messages) diff --git a/lib/features/messaging/presentation/screens/chat_screen.dart b/lib/features/messaging/presentation/screens/chat_screen.dart index 1d43f28..68aeaba 100644 --- a/lib/features/messaging/presentation/screens/chat_screen.dart +++ b/lib/features/messaging/presentation/screens/chat_screen.dart @@ -9,6 +9,7 @@ import 'package:real_estate_mobile/core/utils/image_url_resolver.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; import 'package:real_estate_mobile/features/messaging/data/models/messaging_models.dart'; +import 'package:real_estate_mobile/core/services/push_notification_service.dart'; import 'package:real_estate_mobile/features/messaging/data/socket_service.dart'; import 'package:real_estate_mobile/features/messaging/presentation/providers/messaging_provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -40,6 +41,8 @@ class _ChatScreenState extends ConsumerState { notifier.setActiveConversation(widget.conversationId); notifier.loadMessages(widget.conversationId); notifier.markAsRead(widget.conversationId); + // Suppress push notifications for this conversation while viewing + PushNotificationService().activeConversationId = widget.conversationId; // Join socket room for real-time updates if (_socket.isConnected) { _socket.joinConversation(widget.conversationId); @@ -65,6 +68,7 @@ class _ChatScreenState extends ConsumerState { } _socket.leaveConversation(widget.conversationId); ref.read(messagingProvider.notifier).setActiveConversation(null); + PushNotificationService().activeConversationId = null; _messageController.dispose(); _scrollController.dispose(); _messageFocusNode.dispose(); @@ -1124,41 +1128,38 @@ class _ChatScreenState extends ConsumerState { return Align( alignment: Alignment.centerLeft, child: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildDot(0), - const SizedBox(width: 4), - _buildDot(1), - const SizedBox(width: 4), - _buildDot(2), - ], + padding: const EdgeInsets.only(left: 24, bottom: 8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: AppColors.primaryDark.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(15), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _BouncingDot(delay: 0), + const SizedBox(width: 4), + _BouncingDot(delay: 150), + const SizedBox(width: 4), + _BouncingDot(delay: 300), + const SizedBox(width: 8), + Text( + _formatTime(DateTime.now().toIso8601String()), + style: TextStyle( + fontFamily: 'SourceSerif4', + fontSize: 12, + fontWeight: FontWeight.w400, + color: AppColors.primaryDark.withValues(alpha: 0.5), + ), + ), + ], + ), ), ), ); } - Widget _buildDot(int index) { - return TweenAnimationBuilder( - tween: Tween(begin: 0.3, end: 1.0), - duration: Duration(milliseconds: 600 + (index * 200)), - builder: (context, value, child) { - return Opacity( - opacity: value, - child: Container( - width: 6, - height: 6, - decoration: const BoxDecoration( - color: AppColors.subtleText, - shape: BoxShape.circle, - ), - ), - ); - }, - ); - } - // ============================================================ // IMAGE FULL SCREEN VIEWER // ============================================================ @@ -1477,3 +1478,59 @@ class _AvatarFallback extends StatelessWidget { ); } } + +/// Bouncing dot animation matching web's animate-bounce with staggered delay. +class _BouncingDot extends StatefulWidget { + final int delay; + const _BouncingDot({required this.delay}); + + @override + State<_BouncingDot> createState() => _BouncingDotState(); +} + +class _BouncingDotState extends State<_BouncingDot> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 600), + ); + _animation = Tween(begin: 0, end: -6).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + Future.delayed(Duration(milliseconds: widget.delay), () { + if (mounted) _controller.repeat(reverse: true); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Transform.translate( + offset: Offset(0, _animation.value), + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: AppColors.primaryDark.withValues(alpha: 0.5), + shape: BoxShape.circle, + ), + ), + ); + }, + ); + } +}