fix: improve chat auto-scroll logic, reduce socket timeout, and prevent duplicate message sends

This commit is contained in:
pradeepkumar
2026-03-28 18:50:25 +05:30
parent 130583c98c
commit d9399a7be3
3 changed files with 34 additions and 13 deletions

View File

@@ -277,7 +277,7 @@ class SocketService {
});
return completer.future.timeout(
const Duration(seconds: 10),
const Duration(seconds: 5),
onTimeout: () => null,
);
}

View File

@@ -366,6 +366,7 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
Future<void> 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<MessagingState> {
);
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<MessagingState> {
});
}
// 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<ChatMessage>.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 = <String>{};
messagesAfterSend.removeWhere((m) => !seen.add(m.id));
final finalMessages =
Map<String, List<ChatMessage>>.from(state.messages)
..[conversationId] = messagesAfterSend;
@@ -457,6 +472,8 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
isSending: false,
);
} catch (e) {
if (!mounted) return;
final messagesAfterError =
List<ChatMessage>.from(state.messages[conversationId] ?? []);
messagesAfterError.removeWhere((m) => m.id == tempId);
@@ -465,7 +482,6 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
Map<String, List<ChatMessage>>.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';

View File

@@ -360,10 +360,15 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
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;