From d9399a7be33526a958e5137608df3a08c5280c00 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Sat, 28 Mar 2026 18:50:25 +0530 Subject: [PATCH] fix: improve chat auto-scroll logic, reduce socket timeout, and prevent duplicate message sends --- .../messaging/data/socket_service.dart | 2 +- .../providers/messaging_provider.dart | 34 ++++++++++++++----- .../presentation/screens/chat_screen.dart | 11 ++++-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/features/messaging/data/socket_service.dart b/lib/features/messaging/data/socket_service.dart index dcfcbd1..0ac01de 100644 --- a/lib/features/messaging/data/socket_service.dart +++ b/lib/features/messaging/data/socket_service.dart @@ -277,7 +277,7 @@ class SocketService { }); return completer.future.timeout( - const Duration(seconds: 10), + const Duration(seconds: 5), onTimeout: () => null, ); } diff --git a/lib/features/messaging/presentation/providers/messaging_provider.dart b/lib/features/messaging/presentation/providers/messaging_provider.dart index b65d1e2..ef5d3ed 100644 --- a/lib/features/messaging/presentation/providers/messaging_provider.dart +++ b/lib/features/messaging/presentation/providers/messaging_provider.dart @@ -366,6 +366,7 @@ class MessagingNotifier extends StateNotifier { Future sendMessage(String conversationId, String content) async { final currentUserId = _currentUserId; if (currentUserId == null) return; + if (state.isSending) return; // Prevent double-send final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}'; final now = DateTime.now().toIso8601String(); @@ -401,11 +402,9 @@ class MessagingNotifier extends StateNotifier { ); try { - // Send via socket so the backend broadcasts new_message to the other user. - // The REST endpoint does NOT emit socket events, so using REST here - // would prevent real-time delivery to the other participant. ChatMessage? realMessage; + // Try socket first (enables real-time delivery to other user) if (_socket.isConnected) { realMessage = await _socket.sendMessage(conversationId, { 'content': content, @@ -413,19 +412,35 @@ class MessagingNotifier extends StateNotifier { }); } - // Fallback to REST if socket send failed or not connected - realMessage ??= await _repository.sendMessage( - conversationId, - content: content, - ); + // Fallback to REST if socket failed or not connected + if (realMessage == null) { + try { + realMessage = await _repository.sendMessage( + conversationId, + content: content, + ); + } catch (restError) { + _log.e('REST send also failed', error: restError); + rethrow; + } + } + + if (!mounted) return; final messagesAfterSend = List.from(state.messages[conversationId] ?? []); final tempIndex = messagesAfterSend.indexWhere((m) => m.id == tempId); if (tempIndex != -1) { messagesAfterSend[tempIndex] = realMessage; + } else { + // Temp message was removed (e.g., by a reload) — add the real one + messagesAfterSend.insert(0, realMessage); } + // Deduplicate — socket broadcast might have already added this message + final seen = {}; + messagesAfterSend.removeWhere((m) => !seen.add(m.id)); + final finalMessages = Map>.from(state.messages) ..[conversationId] = messagesAfterSend; @@ -457,6 +472,8 @@ class MessagingNotifier extends StateNotifier { isSending: false, ); } catch (e) { + if (!mounted) return; + final messagesAfterError = List.from(state.messages[conversationId] ?? []); messagesAfterError.removeWhere((m) => m.id == tempId); @@ -465,7 +482,6 @@ class MessagingNotifier extends StateNotifier { Map>.from(state.messages) ..[conversationId] = messagesAfterError; - // Show specific error for connection-required failures final errorMsg = e.toString().contains('connection') ? 'You must be connected to send messages' : 'Failed to send message'; diff --git a/lib/features/messaging/presentation/screens/chat_screen.dart b/lib/features/messaging/presentation/screens/chat_screen.dart index 25283b5..9a57964 100644 --- a/lib/features/messaging/presentation/screens/chat_screen.dart +++ b/lib/features/messaging/presentation/screens/chat_screen.dart @@ -360,10 +360,15 @@ class _ChatScreenState extends ConsumerState orElse: () => null, ); + // Auto-scroll only when a NEW message is added (not on any rebuild) if (messages.length > _previousMessageCount && _previousMessageCount > 0) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(animated: true); - }); + // Only scroll if user is near the bottom (not scrolled up reading history) + if (_scrollController.hasClients && + _scrollController.position.pixels < 150) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _scrollToBottom(animated: true); + }); + } } _previousMessageCount = messages.length;