feat: Implement real-time messaging with socket listeners and enable starting chats from agent details.
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logger/logger.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/features/messaging/data/messaging_repository.dart';
|
||||
import 'package:real_estate_mobile/features/messaging/data/socket_service.dart';
|
||||
|
||||
final _log = Logger(printer: PrettyPrinter(methodCount: 0));
|
||||
|
||||
@@ -66,12 +69,121 @@ class MessagingState {
|
||||
class MessagingNotifier extends StateNotifier<MessagingState> {
|
||||
final MessagingRepository _repository;
|
||||
final Ref _ref;
|
||||
final SocketService _socket = SocketService();
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
MessagingNotifier(this._repository, this._ref)
|
||||
: super(const MessagingState());
|
||||
: super(const MessagingState()) {
|
||||
_initSocketListeners();
|
||||
}
|
||||
|
||||
String? get _currentUserId => _ref.read(authProvider).user?.id;
|
||||
|
||||
void _initSocketListeners() {
|
||||
// Online/offline status changes
|
||||
_subscriptions.add(_socket.onStatusChange.listen((data) {
|
||||
final userId = data['userId'] as String?;
|
||||
final isOnline = data['isOnline'] as bool? ?? false;
|
||||
final lastSeenAt = data['lastSeenAt'] as String?;
|
||||
if (userId != null) {
|
||||
updateUserStatus(userId, isOnline, lastSeenAt: lastSeenAt);
|
||||
}
|
||||
}));
|
||||
|
||||
// Incoming messages from other users
|
||||
_subscriptions.add(_socket.onNewMessage.listen((message) {
|
||||
if (!mounted) return;
|
||||
final convId = message.conversationId;
|
||||
final currentMessages =
|
||||
List<ChatMessage>.from(state.messages[convId] ?? []);
|
||||
|
||||
// Avoid duplicates
|
||||
if (currentMessages.any((m) => m.id == message.id)) return;
|
||||
|
||||
currentMessages.insert(0, message);
|
||||
final updatedMessages =
|
||||
Map<String, List<ChatMessage>>.from(state.messages)
|
||||
..[convId] = currentMessages;
|
||||
|
||||
// Update conversation's last message and unread count
|
||||
final updatedConversations = state.conversations.map((c) {
|
||||
if (c.id == convId) {
|
||||
final isActive = state.activeConversationId == convId;
|
||||
return c.copyWith(
|
||||
lastMessageText: message.content,
|
||||
lastMessageAt: message.createdAt,
|
||||
unreadCount: isActive ? c.unreadCount : c.unreadCount + 1,
|
||||
);
|
||||
}
|
||||
return c;
|
||||
}).toList();
|
||||
|
||||
state = state.copyWith(
|
||||
messages: updatedMessages,
|
||||
conversations: updatedConversations,
|
||||
);
|
||||
}));
|
||||
|
||||
// Typing indicators
|
||||
_subscriptions.add(_socket.onTypingStart.listen((data) {
|
||||
if (!mounted) return;
|
||||
final userId = data['userId'] as String?;
|
||||
if (userId != null && userId != _currentUserId) {
|
||||
state = state.copyWith(
|
||||
typingUsers: {...state.typingUsers, userId},
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
_subscriptions.add(_socket.onTypingStop.listen((data) {
|
||||
if (!mounted) return;
|
||||
final userId = data['userId'] as String?;
|
||||
if (userId != null) {
|
||||
final updated = Set<String>.from(state.typingUsers)..remove(userId);
|
||||
state = state.copyWith(typingUsers: updated);
|
||||
}
|
||||
}));
|
||||
|
||||
// Read receipts
|
||||
_subscriptions.add(_socket.onMessagesRead.listen((data) {
|
||||
if (!mounted) return;
|
||||
final convId = data['conversationId'] as String?;
|
||||
if (convId == null) return;
|
||||
final updatedConversations = state.conversations.map((c) {
|
||||
if (c.id == convId) return c.copyWith(unreadCount: 0);
|
||||
return c;
|
||||
}).toList();
|
||||
state = state.copyWith(conversations: updatedConversations);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Update online status for a user across all conversations (matching web)
|
||||
void updateUserStatus(String userId, bool isOnline, {String? lastSeenAt}) {
|
||||
if (!mounted) return;
|
||||
final updatedConversations = state.conversations.map((c) {
|
||||
if (c.otherParty.userId == userId || c.otherParty.id == userId) {
|
||||
return c.copyWith(
|
||||
otherParty: c.otherParty.copyWith(
|
||||
isOnline: isOnline,
|
||||
lastSeenAt: lastSeenAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
return c;
|
||||
}).toList();
|
||||
|
||||
state = state.copyWith(conversations: updatedConversations);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final sub in _subscriptions) {
|
||||
sub.cancel();
|
||||
}
|
||||
_subscriptions.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> loadConversations() async {
|
||||
state = state.copyWith(isLoadingConversations: true, clearError: true);
|
||||
try {
|
||||
@@ -264,11 +376,14 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
||||
}
|
||||
|
||||
Future<String?> startConversation(String agentProfileId) async {
|
||||
if (!mounted) return null;
|
||||
state = state.copyWith(isLoadingConversations: true, clearError: true);
|
||||
try {
|
||||
final conversation =
|
||||
await _repository.startConversation(agentProfileId);
|
||||
|
||||
if (!mounted) return conversation.id;
|
||||
|
||||
final exists = state.conversations.any((c) => c.id == conversation.id);
|
||||
final updatedConversations = exists
|
||||
? state.conversations
|
||||
@@ -280,10 +395,12 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
||||
);
|
||||
return conversation.id;
|
||||
} catch (_) {
|
||||
state = state.copyWith(
|
||||
isLoadingConversations: false,
|
||||
error: 'Failed to start conversation',
|
||||
);
|
||||
if (mounted) {
|
||||
state = state.copyWith(
|
||||
isLoadingConversations: false,
|
||||
error: 'Failed to start conversation',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user