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:
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user