fix: implement safe map casting for socket events and add support for conversation deletion updates

This commit is contained in:
pradeepkumar
2026-04-09 16:02:31 +05:30
parent f9c6d5e5da
commit 44278b7eb0
3 changed files with 63 additions and 16 deletions

View File

@@ -217,14 +217,21 @@ class MessageSender {
role: json['role'] as String? ?? '', role: json['role'] as String? ?? '',
userProfile: json['userProfile'] != null userProfile: json['userProfile'] != null
? SenderProfile.fromJson( ? SenderProfile.fromJson(
json['userProfile'] as Map<String, dynamic>) _safeMap(json['userProfile']))
: null, : null,
agentProfile: json['agentProfile'] != null agentProfile: json['agentProfile'] != null
? SenderProfile.fromJson( ? SenderProfile.fromJson(
json['agentProfile'] as Map<String, dynamic>) _safeMap(json['agentProfile']))
: null, : null,
); );
} }
/// socket.io in Dart can deliver nested maps as Map<dynamic, dynamic>
static Map<String, dynamic> _safeMap(dynamic val) {
if (val is Map<String, dynamic>) return val;
if (val is Map) return Map<String, dynamic>.from(val);
return <String, dynamic>{};
}
} }
class ChatMessage { class ChatMessage {
@@ -301,7 +308,7 @@ class ChatMessage {
createdAt: json['createdAt'] as String, createdAt: json['createdAt'] as String,
updatedAt: json['updatedAt'] as String, updatedAt: json['updatedAt'] as String,
sender: json['sender'] != null sender: json['sender'] != null
? MessageSender.fromJson(json['sender'] as Map<String, dynamic>) ? MessageSender.fromJson(MessageSender._safeMap(json['sender']))
: MessageSender(id: json['senderId'] as String, role: ''), : MessageSender(id: json['senderId'] as String, role: ''),
); );
} }

View File

@@ -37,6 +37,7 @@ class SocketService {
final _connectionRequestController = StreamController<Map<String, dynamic>>.broadcast(); final _connectionRequestController = StreamController<Map<String, dynamic>>.broadcast();
final _connectionResponseController = StreamController<Map<String, dynamic>>.broadcast(); final _connectionResponseController = StreamController<Map<String, dynamic>>.broadcast();
final _deliveredController = StreamController<Map<String, dynamic>>.broadcast(); final _deliveredController = StreamController<Map<String, dynamic>>.broadcast();
final _conversationDeletedController = StreamController<Map<String, dynamic>>.broadcast();
Stream<ChatMessage> get onNewMessage => _messageController.stream; Stream<ChatMessage> get onNewMessage => _messageController.stream;
Stream<Map<String, dynamic>> get onTypingStart => _typingStartController.stream; Stream<Map<String, dynamic>> get onTypingStart => _typingStartController.stream;
@@ -50,8 +51,18 @@ class SocketService {
Stream<Map<String, dynamic>> get onConnectionRequest => _connectionRequestController.stream; Stream<Map<String, dynamic>> get onConnectionRequest => _connectionRequestController.stream;
Stream<Map<String, dynamic>> get onConnectionResponse => _connectionResponseController.stream; Stream<Map<String, dynamic>> get onConnectionResponse => _connectionResponseController.stream;
Stream<Map<String, dynamic>> get onMessageDelivered => _deliveredController.stream; Stream<Map<String, dynamic>> get onMessageDelivered => _deliveredController.stream;
Stream<Map<String, dynamic>> get onConversationDeleted => _conversationDeletedController.stream;
bool get isConnected => _isConnected; bool get isConnected => _isConnected;
/// Safely cast socket.io data to Map<String, dynamic>.
/// socket_io_client in Dart sometimes delivers maps as Map<dynamic, dynamic>
/// which fails a direct `as Map<String, dynamic>` cast and silently drops events.
static Map<String, dynamic> _toStringMap(dynamic data) {
if (data is Map<String, dynamic>) return data;
if (data is Map) return Map<String, dynamic>.from(data);
return <String, dynamic>{};
}
Future<void> connect() async { Future<void> connect() async {
final token = await SecureStorage.getAccessToken(); final token = await SecureStorage.getAccessToken();
if (token == null) { if (token == null) {
@@ -153,7 +164,9 @@ class SocketService {
// Listen for events // Listen for events
_socket!.on('new_message', (data) { _socket!.on('new_message', (data) {
try { try {
final message = ChatMessage.fromJson(data as Map<String, dynamic>); // socket.io in Dart may deliver data as Map<dynamic, dynamic> — cast safely
final json = _toStringMap(data);
final message = ChatMessage.fromJson(json);
// Auto-ack delivery so sender gets real-time double-tick // Auto-ack delivery so sender gets real-time double-tick
try { try {
_socket?.emit('message_received', { _socket?.emit('message_received', {
@@ -162,31 +175,31 @@ class SocketService {
}); });
} catch (_) {} } catch (_) {}
_messageController.add(message); _messageController.add(message);
} catch (e) { } catch (e, stack) {
_log.e('Error parsing new_message: $e'); _log.e('Error parsing new_message: $e\nRaw data: $data', error: e, stackTrace: stack);
} }
}); });
_socket!.on('typing_start', (data) { _socket!.on('typing_start', (data) {
_typingStartController.add(Map<String, dynamic>.from(data as Map)); _typingStartController.add(_toStringMap(data));
}); });
_socket!.on('typing_stop', (data) { _socket!.on('typing_stop', (data) {
_typingStopController.add(Map<String, dynamic>.from(data as Map)); _typingStopController.add(_toStringMap(data));
}); });
_socket!.on('user_status_change', (data) { _socket!.on('user_status_change', (data) {
_statusController.add(Map<String, dynamic>.from(data as Map)); _statusController.add(_toStringMap(data));
}); });
_socket!.on('messages_read', (data) { _socket!.on('messages_read', (data) {
_readController.add(Map<String, dynamic>.from(data as Map)); _readController.add(_toStringMap(data));
}); });
// Support chat events // Support chat events
_socket!.on('support_new_message', (data) { _socket!.on('support_new_message', (data) {
try { try {
final message = SupportMessage.fromJson(data as Map<String, dynamic>); final message = SupportMessage.fromJson(_toStringMap(data));
_supportMessageController.add(message); _supportMessageController.add(message);
} catch (e) { } catch (e) {
_log.e('Error parsing support_new_message: $e'); _log.e('Error parsing support_new_message: $e');
@@ -194,24 +207,29 @@ class SocketService {
}); });
_socket!.on('support_typing_start', (data) { _socket!.on('support_typing_start', (data) {
_supportTypingStartController.add(Map<String, dynamic>.from(data as Map)); _supportTypingStartController.add(_toStringMap(data));
}); });
_socket!.on('support_typing_stop', (data) { _socket!.on('support_typing_stop', (data) {
_supportTypingStopController.add(Map<String, dynamic>.from(data as Map)); _supportTypingStopController.add(_toStringMap(data));
}); });
// Connection request events (real-time updates) // Connection request events (real-time updates)
_socket!.on('connection_request', (data) { _socket!.on('connection_request', (data) {
_connectionRequestController.add(Map<String, dynamic>.from(data as Map)); _connectionRequestController.add(_toStringMap(data));
}); });
_socket!.on('connection_response', (data) { _socket!.on('connection_response', (data) {
_connectionResponseController.add(Map<String, dynamic>.from(data as Map)); _connectionResponseController.add(_toStringMap(data));
}); });
_socket!.on('message_delivered', (data) { _socket!.on('message_delivered', (data) {
_deliveredController.add(Map<String, dynamic>.from(data as Map)); _deliveredController.add(_toStringMap(data));
});
// Conversation deleted by the other party — remove from local state
_socket!.on('conversation_deleted', (data) {
_conversationDeletedController.add(_toStringMap(data));
}); });
_socket!.connect(); _socket!.connect();
@@ -351,5 +369,6 @@ class SocketService {
_connectionRequestController.close(); _connectionRequestController.close();
_connectionResponseController.close(); _connectionResponseController.close();
_deliveredController.close(); _deliveredController.close();
_conversationDeletedController.close();
} }
} }

View File

@@ -240,6 +240,27 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
state = state.copyWith(conversations: updatedConversations, messages: newMessages); state = state.copyWith(conversations: updatedConversations, messages: newMessages);
})); }));
// Conversation deleted by other party — remove from local state
_subscriptions.add(_socket.onConversationDeleted.listen((data) {
if (!mounted) return;
final convId = data['conversationId'] as String?;
if (convId == null) return;
final updatedMessages =
Map<String, List<ChatMessage>>.from(state.messages)..remove(convId);
final updatedConversations =
state.conversations.where((c) => c.id != convId).toList();
final totalUnread = updatedConversations.fold<int>(
0, (sum, c) => sum + c.unreadCount,
);
state = state.copyWith(
messages: updatedMessages,
conversations: updatedConversations,
unreadCount: totalUnread,
clearActiveConversation: state.activeConversationId == convId,
);
}));
// Reconnection: re-join active conversation room and catch up missed messages // Reconnection: re-join active conversation room and catch up missed messages
_subscriptions.add(_socket.onConnectionChange.listen((connected) { _subscriptions.add(_socket.onConnectionChange.listen((connected) {
if (!mounted || !connected) return; if (!mounted || !connected) return;