fix: implement safe map casting for socket events and add support for conversation deletion updates
This commit is contained in:
@@ -217,14 +217,21 @@ class MessageSender {
|
||||
role: json['role'] as String? ?? '',
|
||||
userProfile: json['userProfile'] != null
|
||||
? SenderProfile.fromJson(
|
||||
json['userProfile'] as Map<String, dynamic>)
|
||||
_safeMap(json['userProfile']))
|
||||
: null,
|
||||
agentProfile: json['agentProfile'] != null
|
||||
? SenderProfile.fromJson(
|
||||
json['agentProfile'] as Map<String, dynamic>)
|
||||
_safeMap(json['agentProfile']))
|
||||
: 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 {
|
||||
@@ -301,7 +308,7 @@ class ChatMessage {
|
||||
createdAt: json['createdAt'] as String,
|
||||
updatedAt: json['updatedAt'] as String,
|
||||
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: ''),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class SocketService {
|
||||
final _connectionRequestController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _connectionResponseController = 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<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 onConnectionResponse => _connectionResponseController.stream;
|
||||
Stream<Map<String, dynamic>> get onMessageDelivered => _deliveredController.stream;
|
||||
Stream<Map<String, dynamic>> get onConversationDeleted => _conversationDeletedController.stream;
|
||||
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 {
|
||||
final token = await SecureStorage.getAccessToken();
|
||||
if (token == null) {
|
||||
@@ -153,7 +164,9 @@ class SocketService {
|
||||
// Listen for events
|
||||
_socket!.on('new_message', (data) {
|
||||
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
|
||||
try {
|
||||
_socket?.emit('message_received', {
|
||||
@@ -162,31 +175,31 @@ class SocketService {
|
||||
});
|
||||
} catch (_) {}
|
||||
_messageController.add(message);
|
||||
} catch (e) {
|
||||
_log.e('Error parsing new_message: $e');
|
||||
} catch (e, stack) {
|
||||
_log.e('Error parsing new_message: $e\nRaw data: $data', error: e, stackTrace: stack);
|
||||
}
|
||||
});
|
||||
|
||||
_socket!.on('typing_start', (data) {
|
||||
_typingStartController.add(Map<String, dynamic>.from(data as Map));
|
||||
_typingStartController.add(_toStringMap(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) {
|
||||
_statusController.add(Map<String, dynamic>.from(data as Map));
|
||||
_statusController.add(_toStringMap(data));
|
||||
});
|
||||
|
||||
_socket!.on('messages_read', (data) {
|
||||
_readController.add(Map<String, dynamic>.from(data as Map));
|
||||
_readController.add(_toStringMap(data));
|
||||
});
|
||||
|
||||
// Support chat events
|
||||
_socket!.on('support_new_message', (data) {
|
||||
try {
|
||||
final message = SupportMessage.fromJson(data as Map<String, dynamic>);
|
||||
final message = SupportMessage.fromJson(_toStringMap(data));
|
||||
_supportMessageController.add(message);
|
||||
} catch (e) {
|
||||
_log.e('Error parsing support_new_message: $e');
|
||||
@@ -194,24 +207,29 @@ class SocketService {
|
||||
});
|
||||
|
||||
_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) {
|
||||
_supportTypingStopController.add(Map<String, dynamic>.from(data as Map));
|
||||
_supportTypingStopController.add(_toStringMap(data));
|
||||
});
|
||||
|
||||
// Connection request events (real-time updates)
|
||||
_socket!.on('connection_request', (data) {
|
||||
_connectionRequestController.add(Map<String, dynamic>.from(data as Map));
|
||||
_connectionRequestController.add(_toStringMap(data));
|
||||
});
|
||||
|
||||
_socket!.on('connection_response', (data) {
|
||||
_connectionResponseController.add(Map<String, dynamic>.from(data as Map));
|
||||
_connectionResponseController.add(_toStringMap(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();
|
||||
@@ -351,5 +369,6 @@ class SocketService {
|
||||
_connectionRequestController.close();
|
||||
_connectionResponseController.close();
|
||||
_deliveredController.close();
|
||||
_conversationDeletedController.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +240,27 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
||||
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
|
||||
_subscriptions.add(_socket.onConnectionChange.listen((connected) {
|
||||
if (!mounted || !connected) return;
|
||||
|
||||
Reference in New Issue
Block a user