diff --git a/lib/features/messaging/data/models/messaging_models.dart b/lib/features/messaging/data/models/messaging_models.dart index 4bc6912..fc14b8b 100644 --- a/lib/features/messaging/data/models/messaging_models.dart +++ b/lib/features/messaging/data/models/messaging_models.dart @@ -217,14 +217,21 @@ class MessageSender { role: json['role'] as String? ?? '', userProfile: json['userProfile'] != null ? SenderProfile.fromJson( - json['userProfile'] as Map) + _safeMap(json['userProfile'])) : null, agentProfile: json['agentProfile'] != null ? SenderProfile.fromJson( - json['agentProfile'] as Map) + _safeMap(json['agentProfile'])) : null, ); } + + /// socket.io in Dart can deliver nested maps as Map + static Map _safeMap(dynamic val) { + if (val is Map) return val; + if (val is Map) return Map.from(val); + return {}; + } } 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) + ? MessageSender.fromJson(MessageSender._safeMap(json['sender'])) : MessageSender(id: json['senderId'] as String, role: ''), ); } diff --git a/lib/features/messaging/data/socket_service.dart b/lib/features/messaging/data/socket_service.dart index 7c8b07c..8446b77 100644 --- a/lib/features/messaging/data/socket_service.dart +++ b/lib/features/messaging/data/socket_service.dart @@ -37,6 +37,7 @@ class SocketService { final _connectionRequestController = StreamController>.broadcast(); final _connectionResponseController = StreamController>.broadcast(); final _deliveredController = StreamController>.broadcast(); + final _conversationDeletedController = StreamController>.broadcast(); Stream get onNewMessage => _messageController.stream; Stream> get onTypingStart => _typingStartController.stream; @@ -50,8 +51,18 @@ class SocketService { Stream> get onConnectionRequest => _connectionRequestController.stream; Stream> get onConnectionResponse => _connectionResponseController.stream; Stream> get onMessageDelivered => _deliveredController.stream; + Stream> get onConversationDeleted => _conversationDeletedController.stream; bool get isConnected => _isConnected; + /// Safely cast socket.io data to Map. + /// socket_io_client in Dart sometimes delivers maps as Map + /// which fails a direct `as Map` cast and silently drops events. + static Map _toStringMap(dynamic data) { + if (data is Map) return data; + if (data is Map) return Map.from(data); + return {}; + } + Future 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); + // socket.io in Dart may deliver data as Map — 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.from(data as Map)); + _typingStartController.add(_toStringMap(data)); }); _socket!.on('typing_stop', (data) { - _typingStopController.add(Map.from(data as Map)); + _typingStopController.add(_toStringMap(data)); }); _socket!.on('user_status_change', (data) { - _statusController.add(Map.from(data as Map)); + _statusController.add(_toStringMap(data)); }); _socket!.on('messages_read', (data) { - _readController.add(Map.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); + 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.from(data as Map)); + _supportTypingStartController.add(_toStringMap(data)); }); _socket!.on('support_typing_stop', (data) { - _supportTypingStopController.add(Map.from(data as Map)); + _supportTypingStopController.add(_toStringMap(data)); }); // Connection request events (real-time updates) _socket!.on('connection_request', (data) { - _connectionRequestController.add(Map.from(data as Map)); + _connectionRequestController.add(_toStringMap(data)); }); _socket!.on('connection_response', (data) { - _connectionResponseController.add(Map.from(data as Map)); + _connectionResponseController.add(_toStringMap(data)); }); _socket!.on('message_delivered', (data) { - _deliveredController.add(Map.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(); } } diff --git a/lib/features/messaging/presentation/providers/messaging_provider.dart b/lib/features/messaging/presentation/providers/messaging_provider.dart index 3669bc6..632afa8 100644 --- a/lib/features/messaging/presentation/providers/messaging_provider.dart +++ b/lib/features/messaging/presentation/providers/messaging_provider.dart @@ -240,6 +240,27 @@ class MessagingNotifier extends StateNotifier { 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>.from(state.messages)..remove(convId); + final updatedConversations = + state.conversations.where((c) => c.id != convId).toList(); + final totalUnread = updatedConversations.fold( + 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;