fix: improve chat auto-scroll logic, reduce socket timeout, and prevent duplicate message sends
This commit is contained in:
@@ -277,7 +277,7 @@ class SocketService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return completer.future.timeout(
|
return completer.future.timeout(
|
||||||
const Duration(seconds: 10),
|
const Duration(seconds: 5),
|
||||||
onTimeout: () => null,
|
onTimeout: () => null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -366,6 +366,7 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
|||||||
Future<void> sendMessage(String conversationId, String content) async {
|
Future<void> sendMessage(String conversationId, String content) async {
|
||||||
final currentUserId = _currentUserId;
|
final currentUserId = _currentUserId;
|
||||||
if (currentUserId == null) return;
|
if (currentUserId == null) return;
|
||||||
|
if (state.isSending) return; // Prevent double-send
|
||||||
|
|
||||||
final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
|
final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
final now = DateTime.now().toIso8601String();
|
final now = DateTime.now().toIso8601String();
|
||||||
@@ -401,11 +402,9 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
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;
|
ChatMessage? realMessage;
|
||||||
|
|
||||||
|
// Try socket first (enables real-time delivery to other user)
|
||||||
if (_socket.isConnected) {
|
if (_socket.isConnected) {
|
||||||
realMessage = await _socket.sendMessage(conversationId, {
|
realMessage = await _socket.sendMessage(conversationId, {
|
||||||
'content': content,
|
'content': content,
|
||||||
@@ -413,19 +412,35 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to REST if socket send failed or not connected
|
// Fallback to REST if socket failed or not connected
|
||||||
realMessage ??= await _repository.sendMessage(
|
if (realMessage == null) {
|
||||||
conversationId,
|
try {
|
||||||
content: content,
|
realMessage = await _repository.sendMessage(
|
||||||
);
|
conversationId,
|
||||||
|
content: content,
|
||||||
|
);
|
||||||
|
} catch (restError) {
|
||||||
|
_log.e('REST send also failed', error: restError);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
final messagesAfterSend =
|
final messagesAfterSend =
|
||||||
List<ChatMessage>.from(state.messages[conversationId] ?? []);
|
List<ChatMessage>.from(state.messages[conversationId] ?? []);
|
||||||
final tempIndex = messagesAfterSend.indexWhere((m) => m.id == tempId);
|
final tempIndex = messagesAfterSend.indexWhere((m) => m.id == tempId);
|
||||||
if (tempIndex != -1) {
|
if (tempIndex != -1) {
|
||||||
messagesAfterSend[tempIndex] = realMessage;
|
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 =
|
final finalMessages =
|
||||||
Map<String, List<ChatMessage>>.from(state.messages)
|
Map<String, List<ChatMessage>>.from(state.messages)
|
||||||
..[conversationId] = messagesAfterSend;
|
..[conversationId] = messagesAfterSend;
|
||||||
@@ -457,6 +472,8 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
|||||||
isSending: false,
|
isSending: false,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
final messagesAfterError =
|
final messagesAfterError =
|
||||||
List<ChatMessage>.from(state.messages[conversationId] ?? []);
|
List<ChatMessage>.from(state.messages[conversationId] ?? []);
|
||||||
messagesAfterError.removeWhere((m) => m.id == tempId);
|
messagesAfterError.removeWhere((m) => m.id == tempId);
|
||||||
@@ -465,7 +482,6 @@ class MessagingNotifier extends StateNotifier<MessagingState> {
|
|||||||
Map<String, List<ChatMessage>>.from(state.messages)
|
Map<String, List<ChatMessage>>.from(state.messages)
|
||||||
..[conversationId] = messagesAfterError;
|
..[conversationId] = messagesAfterError;
|
||||||
|
|
||||||
// Show specific error for connection-required failures
|
|
||||||
final errorMsg = e.toString().contains('connection')
|
final errorMsg = e.toString().contains('connection')
|
||||||
? 'You must be connected to send messages'
|
? 'You must be connected to send messages'
|
||||||
: 'Failed to send message';
|
: 'Failed to send message';
|
||||||
|
|||||||
@@ -360,10 +360,15 @@ class _ChatScreenState extends ConsumerState<ChatScreen>
|
|||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Auto-scroll only when a NEW message is added (not on any rebuild)
|
||||||
if (messages.length > _previousMessageCount && _previousMessageCount > 0) {
|
if (messages.length > _previousMessageCount && _previousMessageCount > 0) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
// Only scroll if user is near the bottom (not scrolled up reading history)
|
||||||
_scrollToBottom(animated: true);
|
if (_scrollController.hasClients &&
|
||||||
});
|
_scrollController.position.pixels < 150) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (mounted) _scrollToBottom(animated: true);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_previousMessageCount = messages.length;
|
_previousMessageCount = messages.length;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user