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