feat: Implement push notification suppression for active chats, prevent duplicate self-messages, update the chat typing indicator, and add back buttons to static screens.

This commit is contained in:
pradeepkumar
2026-03-18 17:24:32 +05:30
parent cde137e07b
commit a4aa9bfe25
8 changed files with 196 additions and 35 deletions

View File

@@ -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<ChatScreen> {
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<ChatScreen> {
}
_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<ChatScreen> {
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<double>(
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<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
);
_animation = Tween<double>(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,
),
),
);
},
);
}
}